Meet the oBundle Team

Let’s get connected!

Book a time
BigCommerce Company Address Book: Managing B2B Ship-To Addresses from the Storefront GraphQL API

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 by entityId.
  • 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.addAddress
  • company.updateAddress
  • company.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.

Address book requests before and after the October 2026 change Before, a headless storefront called the standard GraphQL Storefront API for catalog and cart, and a separate B2B API host for company addresses. After, both the catalog and the company address book are reachable from the standard GraphQL Storefront API using one storefront token. One storefront, one token: where address calls go now BEFORE Headless storefront storefront token GraphQL Storefront API: catalog, cart, customer second client, B2B token B2B API host: company address book AFTER Headless storefront one storefront token GraphQL Storefront API catalog, cart, customer, activeCompany.addresses

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.

OperationAdminSenior BuyerJunior BuyerCustom role
activeCompany.addresses and addressYesYesYesNeeds View under Addresses
company.addAddressYesNoNoNeeds Create under Addresses
company.updateAddressYesNoNoNeeds Edit under Addresses
company.deleteAddressYesNoNoNeeds 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.

The two gates every address write has to pass An address write request first passes a store settings gate checking that the address book and address administration are enabled, then a role gate checking that the caller is a Company Admin or a custom role with the matching permission. Failing either gate returns PERMISSION_DENIED. Two gates: store settings, then company role addAddress request Gate 1: store settings address_book enabled? address_admin enabled? Gate 2: company role Admin, or custom role with Create + View Written errors: [{ code: "PERMISSION_DENIED" }] HTTP 200, entityId is null Read site.settings.b2b.config.address before rendering write controls, so a denied write never reaches the buyer. Both gates must pass; an Admin can still be denied by store settings.

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:

  1. company.addAddress with the name and location fields. Capture entityId.
  2. company.updateAddress with that entityId, isShipping: true and isDefaultShipping: 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.

The two-call sequence required to add a usable default ship-to address addAddress creates the record with all flags off and returns an entityId. A second updateAddress call sets isShipping and isDefaultShipping together. If the second call fails, the address exists but is unusable at checkout and must be retried or deleted. Adding a usable default ship-to is a two-step transaction 1. company.addAddress name, address1, city, countryCode all flags off, returns entityId entityId 2. company.updateAddress isShipping: true isDefaultShipping: true Selectable at checkout as the default ship-to Step 2 fails: the address exists with no flags set Invisible at checkout. Retry or delete it, do not leave it in the book.

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 typeValueType-specific propertiesWrite under
B2BTextExtraFieldDefinitionSingle-line textmaxLength, defaultTextextraFields.texts
B2BMultilineTextExtraFieldDefinitionMultiline textrows, defaultTextextraFields.multilineTexts
B2BNumberExtraFieldDefinitionNumbermaxValue, defaultNumberextraFields.numbers
B2BDropdownExtraFieldDefinitionSingle choiceoptions, defaultValueextraFields.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 storefrontStorefront GraphQLNote
addressLine1 and addressLine2address1 and address2Straight rename
zipCodepostalCodeStraight rename
phoneNumberphoneStraight rename
country object with countryName and countryCodecountryCode on write, country and countryCode on readWrites take the ISO alpha-2 code only
state object with stateName and stateCodestateOrProvince on write, plus stateOrProvinceCode on readStructure differs, not just the name
companyId in the pathNo company ID anywhereResolved from the session
Flags required on createFlags not accepted on createSecond 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:

  1. Confirm the feature is on. Query site.settings.b2b.config.address and record which of the four toggles are enabled on the store you are building against. Do not assume the sandbox matches production.
  2. 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.
  3. 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_admin is enabled.
  4. Implement create as a two-step transaction with explicit reconciliation on partial failure.
  5. Write the payload error handler before the happy path. HTTP 200 with a populated errors array is the default failure mode here, and it is silent if you do not look for it.
  6. Design the list around search. With CREATED_AT as the only sort, search and filters carry the experience.
  7. Test the masquerade path. If sales reps will manage addresses, verify address_sales_rep is enabled and test as a masquerading Super Admin, not just as a Company Admin.
  8. 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.