BigCommerce Company Address Book: Managing B2B Ship-To Addresses from the Storefront GraphQL API
Ask a B2B merchant where their ecommerce project went sideways and surprisingly often the answer is addresses. Not pricing, not catalog, not punchout. Addresses. A distributor signs a national account, and that one customer record carries forty-three ship-to locations, six of which are job sites that will not exist in March. A buyer at a regional office needs to send a pallet to a branch that was added last week. A procurement lead needs billing to go to corporate accounts payable while every shipment goes somewhere else entirely.
Consumer ecommerce treats the address book as a convenience feature. B2B ecommerce treats it as master data, and when it is wrong, orders go to the wrong dock, invoices go to the wrong entity, and someone in customer service spends their afternoon on the phone instead of selling.
On October 30, 2026, BigCommerce brought the B2B Edition company address book into the standard GraphQL Storefront API. Company Admins can now list, search, create, update, delete and set default addresses using the same API surface and the same token that already powers the rest of a headless storefront. All company users can read the address configuration and extra field definitions.
The changelog entry is three sentences long. The implications are larger than that, and they are the subject of this guide: what shipped, what it replaces, the permission model you have to respect, the two gotchas that will bite you in the first hour, and how to migrate off the legacy REST storefront address endpoints without breaking a live buyer portal.
What shipped: the BigCommerce company address book in Storefront GraphQL
The address book now lives under the signed-in customer's active company context. That phrasing matters, and we will come back to it, because it is also the feature's main constraint.
Four new read paths:
customer.activeCompany.addresses, a paginated, filterable connection of every address in the active company's book.customer.activeCompany.address, a single address looked up byentityId.site.settings.b2b.config.address, the store-level toggles that decide whether the address book exists at all and who may manage it.site.settings.b2b.extraFields.address, the definitions for any custom fields your store has attached to addresses.
Three new mutations, all namespaced under company:
company.addAddresscompany.updateAddresscompany.deleteAddress
The older company.addresses and company.address queries are deprecated in favor of the customer.activeCompany equivalents. The company.* mutations are unaffected by that deprecation, so if you have already written mutation code against them, it keeps working. Only the read paths move.
Why this matters more than the changelog suggests
Until now, a headless BigCommerce B2B storefront had to speak two dialects. Catalog, cart, checkout and customer data came from the GraphQL Storefront API at the standard endpoint. Company objects, quotes, shopping lists and address management came from the B2B API family at api-b2b.bigcommerce.com, either over B2B GraphQL or over the REST storefront endpoints. Two base URLs, two sets of field naming conventions, two mental models for errors, and two places to look when something returns an empty array.
Pulling the address book into the canonical Storefront API is a step toward a single storefront surface. You authenticate once with a storefront token or a customer impersonation token, and the same request that fetches a product can fetch the company's ship-to list. For teams building on Catalyst, that is one fewer client to configure, one fewer token to refresh, and one fewer place for a session mismatch to hide.
It is worth being precise about the scope of that claim. BigCommerce has not announced that all B2B functionality is consolidating into the Storefront GraphQL API. What is documented is that four B2B areas now have Storefront GraphQL guides: company account registration, the company address book, company user account settings, and company user management. That is a direction of travel, not a published roadmap, and you should plan your architecture against what is shipped rather than what the pattern implies.
Authentication and the active company
Every address operation resolves the company from the session. There is no company ID argument anywhere in the API, on any query or mutation. The documentation states this plainly, and it is the single most important thing to internalize before you design around the feature.
Requests carry a bearer token obtained from the Create a storefront token endpoint or the Create a customer impersonation token endpoint. On a Stencil storefront you can also pull a token from the page context. The token has to identify a signed-in customer who is either a B2B company user or a Super Admin. Without that customer context, address operations return nothing. Not an error, nothing.
That has three consequences worth planning for:
- A Super Admin who is not masquerading has no active company, and the address list comes back empty. If you are building an internal sales rep tool, the masquerade session is not optional, it is the mechanism.
- Company hierarchies are invisible. Operations do not reach parent or child companies. A parent company admin cannot see a subsidiary's addresses through this API. Switching companies means switching masquerade sessions.
- An empty list is ambiguous by design. If B2B Edition is off, or the address feature is off, or the caller has no active company, the list is empty rather than erroring. You cannot distinguish those cases from the list response alone, which is exactly why you should read the settings first.
Read the settings before you render anything
The fastest way to ship a broken buyer portal is to render an "Add address" button and discover at submit time that the store has address administration turned off. Query site.settings.b2b.config.address first. It returns toggles as key and isEnabled pairs:
address_book, whether the address book is enabled at all.address_admin, whether users whose role permits it may add, update and delete.address_sales_rep, whether a masquerading Super Admin may manage addresses.address_label, whether address labels are in use.
Both config.address and extraFields.address return null if settings cannot be retrieved. Treat null as "do not render management controls" rather than as a reason to fall through to a default. A read-only address list is a mildly disappointing experience. A write button that always fails is a support ticket.
The permission matrix
Access follows the company role, and the split is sharper than most teams expect: every role can read, but only Admins can write.
| Operation | Admin | Senior Buyer | Junior Buyer | Custom role |
|---|---|---|---|---|
activeCompany.addresses and address | Yes | Yes | Yes | Needs View under Addresses |
company.addAddress | Yes | No | No | Needs Create under Addresses |
company.updateAddress | Yes | No | No | Needs Edit under Addresses |
company.deleteAddress | Yes | No | No | Needs Delete under Addresses |
Two details to hold on to. First, each of create, edit and delete also requires the view permission on custom roles, so a custom role with Create but not View will not behave the way its name suggests. Second, role permission is necessary but not sufficient: the store-level settings above can still deny the mutation, and when they do you get PERMISSION_DENIED even for an Admin.
A masquerading Super Admin has Admin-equivalent permissions for the company they are masquerading as, assuming address_sales_rep is enabled. This is how a sales rep adds a ship-to on the phone without logging into the control panel.
Creating an address takes two calls, not one
This is the gotcha that will cost you an afternoon if nobody warns you, so here it is in bold: addAddress cannot set the shipping, billing or default flags. A new address starts with all four flags off. It exists in the book and it is unusable at checkout until you follow up with updateAddress.
The required inputs on addAddress are firstName, lastName, address1, city and countryCode, where the country code is ISO 3166-1 alpha-2, so US rather than USA or United States. Optional inputs are address2, stateOrProvince, postalCode, phone, label and extraFields. Any extra field marked required in the definitions must be supplied or the mutation fails validation.
The mutation returns entityId and an errors array. On success errors is empty and entityId is the new address ID. On failure entityId is null.
There is also no dedicated "set default" mutation. You set a default by calling updateAddress with isDefaultShipping or isDefaultBilling set to true, and the two defaults are independent of each other. One further rule: an address must be enabled for a purpose before it can be the default for that purpose. Setting isDefaultShipping on an address where isShipping is false returns VALIDATION_ERROR. Set both in the same request and you avoid the round trip.
So the real create flow for "add this warehouse and make it the default ship-to" is:
company.addAddresswith the name and location fields. CaptureentityId.company.updateAddresswith thatentityId,isShipping: trueandisDefaultShipping: true.
Two network calls, and the second one can fail independently of the first. Build your UI to treat creation as a transaction you have to reconcile: if step two fails, you have an orphaned address with no flags set, sitting in the buyer's book doing nothing. Either retry, or delete and surface a clear error. Silently leaving it there is how an address book accumulates ghosts.
Listing, searching and the sort you do not get
The addresses connection is cursor paginated in the usual Relay shape: first, after, last, before, with pageInfo carrying hasNextPage and endCursor, and results arriving as edges containing node.
Each node exposes entityId, label, firstName, lastName, address1, address2, city, stateOrProvince, stateOrProvinceCode, postalCode, country, countryCode, phone, the four flags isShipping, isBilling, isDefaultShipping and isDefaultBilling, plus createdAt, updatedAt and extraFields.
Filters are optional and combinable: searchQuery, isShipping, isBilling, isDefaultShipping, isDefaultBilling, city, state, country and companyIds. For a distributor whose buyers maintain dozens of locations, searchQuery combined with isShipping: true is the query that makes the checkout address picker usable instead of a scroll of despair.
Now the limitation. Sorting accepts a sort array of field and direction pairs, but field supports exactly one value: CREATED_AT. You can order ascending or descending by creation date and that is the whole menu. There is no sort by label, by city, by last used, or by default status.
If your buyers expect an alphabetical address book, and they generally do, you have two options. Fetch the full set and sort client side, which is fine for a company with forty addresses and unpleasant for one with four hundred. Or maintain your own ordering hint in an extra field and sort on that after fetching. Neither is elegant. Plan the interaction around searchQuery rather than around browsing a sorted list, and the limitation mostly stops mattering.
Extra fields, and the read and write asymmetry
Address extra fields are where most B2B merchants encode the things that make their operation work: dock hours, a receiving contact, a customer-side location code that has to appear on the packing slip, a delivery instruction about the gate on the north side.
The definitions come from site.settings.b2b.extraFields.address, and every definition implements a common interface with fieldName, labelName, isRequired and visibleToEndUser. Note that fieldName is the key you use as name when reading and writing values, while labelName is what you show a human. Mixing those up is a classic first-pass bug.
There are four definition types, and each writes into a differently named bucket:
| Definition type | Value | Type-specific properties | Write under |
|---|---|---|---|
B2BTextExtraFieldDefinition | Single-line text | maxLength, defaultText | extraFields.texts |
B2BMultilineTextExtraFieldDefinition | Multiline text | rows, defaultText | extraFields.multilineTexts |
B2BNumberExtraFieldDefinition | Number | maxValue, defaultNumber | extraFields.numbers |
B2BDropdownExtraFieldDefinition | Single choice | options, defaultValue | extraFields.multipleChoices |
On the read side, extra field values are not returned by default. You have to set includesExtraFields: true on the addresses query and explicitly select extraFields, then unpack the values with inline fragments on TextExtraFieldValue, MultilineTextExtraFieldValue, NumberExtraFieldValue and MultipleChoiceExtraFieldValue.
That opt-in behavior is a performance feature, not an obstacle. Skip extra fields on the checkout address picker, where you need twenty rows fast, and request them on the single-address detail view where the dock hours actually matter.
Error handling: two different shapes
Mutations and queries fail differently, and your client needs to handle both.
Mutation errors arrive in the payload with an HTTP 200. The request succeeded; the operation did not. Check the errors array and expect entityId to be null. The documented codes are VALIDATION_ERROR, PERMISSION_DENIED, UNAUTHORIZED, NOT_FOUND, NO_COMPANY, NOT_AUTHENTICATED, FEATURE_DISABLED, B2B_DISABLED and INTERNAL_ERROR.
If you have an HTTP client that treats 200 as unconditional success, it will cheerfully report that an address was created when it was not. Write the payload error check once, in a shared wrapper, and use it everywhere.
Query errors arrive in the standard GraphQL errors array, with the code in extensions.code. The documented set is smaller: PERMISSION_DENIED, THROTTLED and UPSTREAM_ERROR. The latter two are retryable with backoff. The first is not, and retrying it just burns rate limit.
Map these to user-facing copy deliberately. FEATURE_DISABLED and B2B_DISABLED are merchant configuration problems and the buyer can do nothing about them, so a generic "address management is not available on this account" beats exposing the code. VALIDATION_ERROR is the buyer's to fix and deserves a specific message tied to the field.
Migrating from the B2B REST storefront address endpoints
If you have an existing Stencil or headless build, your address code almost certainly calls the B2B REST storefront endpoints at api-b2b.bigcommerce.com, for example PUT /api/v2/companies/:companyId/addresses/:addressId. Those endpoints are not marked deprecated, and nothing in the October release forces you off them. This is a migration you choose, not one you are handed.
The case for doing it is consolidation: one endpoint, one token, one error model, and the Storefront GraphQL API is where BigCommerce is adding new B2B storefront guides. The case for waiting is that the GraphQL version has real gaps today, chiefly the single sort field and the two-call create.
If you do migrate, the field names do not line up, and that is the part teams underestimate. A sample of the differences:
| B2B REST storefront | Storefront GraphQL | Note |
|---|---|---|
addressLine1 and addressLine2 | address1 and address2 | Straight rename |
zipCode | postalCode | Straight rename |
phoneNumber | phone | Straight rename |
country object with countryName and countryCode | countryCode on write, country and countryCode on read | Writes take the ISO alpha-2 code only |
state object with stateName and stateCode | stateOrProvince on write, plus stateOrProvinceCode on read | Structure differs, not just the name |
companyId in the path | No company ID anywhere | Resolved from the session |
| Flags required on create | Flags not accepted on create | Second call required |
That last row is the one that breaks a naive port. The REST update endpoint requires isShipping, isBilling, isDefaultShipping and isDefaultBilling as required fields. The GraphQL addAddress mutation rejects them outright. Code written against the REST shape will not translate field for field.
A sensible migration order for a live store: move reads first, because they are idempotent and a bad read shows up immediately in QA. Keep writes on the existing path until the read side has been in production for a release cycle. Then move create and update behind a feature flag, with the two-call reconciliation logic tested against a company that has extra fields configured, because extra fields are where the shapes diverge most.
What this does not solve
Three things worth setting expectations on before anyone promises them in a scoping call.
There is no address validation. The API validates field formats and required fields. It does not tell you whether 1420 Industrial Parkway is a real place that a truck can reach. If your delivery failure rate is the actual problem, you still need an address verification service in front of the write, and that is integration work this release does not reduce.
There is no bulk import. Onboarding a national account with forty-three locations means forty-three creates and forty-three updates, driven by your own script against the storefront session. For a one-time migration, the server-to-server B2B Management API remains the better tool. The Storefront GraphQL address book is built for buyers maintaining their own book, not for data loads.
There is no hierarchy traversal. This bears repeating because company hierarchies are common in distribution and the limitation is absolute: no operation accepts a company ID, and nothing reaches parent or child companies. A corporate administrator who needs visibility across subsidiaries needs a different architecture, most likely a server-side service using the Management API with its own permission layer.
An implementation checklist
If you are putting this into a buyer portal in the next sprint, work in this order:
- Confirm the feature is on. Query
site.settings.b2b.config.addressand record which of the four toggles are enabled on the store you are building against. Do not assume the sandbox matches production. - Pull the extra field definitions early. They determine your form, your validation and your write payload shape. Build the form from the definitions rather than hard coding fields, so a merchant adding a field in the control panel does not require a deploy.
- Gate the UI on role and settings together. Senior and Junior Buyers get a read-only view. Admins get write controls, but only if
address_adminis enabled. - Implement create as a two-step transaction with explicit reconciliation on partial failure.
- Write the payload error handler before the happy path. HTTP 200 with a populated
errorsarray is the default failure mode here, and it is silent if you do not look for it. - Design the list around search. With
CREATED_ATas the only sort, search and filters carry the experience. - Test the masquerade path. If sales reps will manage addresses, verify
address_sales_repis enabled and test as a masquerading Super Admin, not just as a Company Admin. - Test the empty states. A non-masquerading Super Admin, a company with no addresses and a store with the feature disabled all produce an empty list. Make sure your UI says something different in each case.
Why this lands differently for B2B merchants
It is easy to read a changelog entry about address queries and file it under plumbing. In B2B it is not plumbing, it is the difference between a buyer self-serving and a buyer calling.
Every address a buyer cannot add themselves becomes a phone call to customer service, a manual record change by someone with control panel access, and a delay between a buyer wanting to order and being able to. Multiply that by a few hundred accounts and the cost is a measurable share of a service team's week. The companies that get B2B ecommerce right are the ones that move that work to the buyer, who knows the answer, and away from the rep, who has to ask.
What changed on October 30 is that you can now build that self-service into a headless storefront with the same client, the same token and the same error handling you already use for everything else. That is less code to write and, more importantly, less code to keep in sync when BigCommerce changes something on one side of the house.
Frequently asked questions
Do I need B2B Edition for the company address book API?
Yes. If B2B Edition is disabled for the store, mutations return B2B_DISABLED and the address list comes back empty. This is a B2B Edition feature exposed through the standard Storefront API, not a feature of the standard API.
Can a Senior Buyer add a shipping address?
Not under the default roles. Only Admins can create, update or delete. A custom role can be granted Create, Edit or Delete under Addresses, and each of those also requires View. If buyers in the field need to add job-site addresses, a custom role is the supported path.
Why is my address list empty when I know the company has addresses?
Most often there is no active company on the session. A Super Admin who is not masquerading has no active company and gets an empty list rather than an error. The other candidates are B2B Edition disabled, the address book toggle off, or a token that does not carry a signed-in customer context.
How do I set a default shipping address?
Call company.updateAddress with isDefaultShipping: true. There is no separate mutation. The address must also have isShipping: true, so set both in the same request if the address is new. Default shipping and default billing are independent.
Can a parent company admin manage a subsidiary's addresses?
Not through this API. No operation accepts a company ID and nothing traverses a company hierarchy. Cross-company management needs a masquerade session per company, or a server-side service built on the B2B Management API.
Should I migrate off the B2B REST storefront address endpoints now?
Only if consolidation is worth the port to you. Those endpoints are not deprecated. The field names differ, the create flow differs, and the GraphQL version currently sorts only by creation date. Moving reads first is low risk; moving writes deserves a feature flag.
Do extra field values come back automatically?
No. Set includesExtraFields: true and select extraFields explicitly, then read typed values through inline fragments. Leaving them out of high-traffic list queries is a deliberate performance choice.
Does this work on Stencil as well as Catalyst?
Yes. The requirement is a storefront bearer token with a signed-in customer context, and on Stencil you can take a token from the page context. The API does not care which storefront technology issued the request.
The bottom line
The BigCommerce company address book in the GraphQL Storefront API is a genuine simplification for anyone building a B2B storefront: one endpoint, one token, read access for every company user and write access for Admins, with store settings as a second gate on top of roles.
Budget for the rough edges. Creating a usable address takes two calls. Sorting is creation date only. Mutation failures hide inside HTTP 200 responses. None of those is a blocker, and all of them are cheaper to design around at the start than to discover in UAT.
If you are running B2B Edition and your buyers still phone in ship-to changes, this is the quarter to fix that. oBundle holds B2B Specialized certification in the BigCommerce Partner Program, and we build this kind of buyer self-service on Stencil and Catalyst storefronts. If you want a second pair of eyes on an address book implementation, or on whether to migrate off the REST endpoints at all, get in touch.