Migrating from v2 to v4
The legacy v2 API shuts down on 1 Jan 2027. This page is the translation guide: what your key becomes, which capabilities replace your scopes, and which contracts change in ways your code will notice.
Work through it in order. The scope mapping is a security decision, not a find-and-replace.
The key changes shape
| Legacy v2 | v4 | |
|---|---|---|
| Prefix | GW.… | grk_sk_… |
| Bound to | A company | A user |
| Reach | Whatever the company's key was granted | Whatever that user's role can see, across every company they administer |
| Lifetime | Independent of any person | Stops working if the bound user is disabled |
There is no in-place upgrade. A GW. key cannot become a grk_sk_ key; you mint a new one. Sending the old key to /v4, /mcp or the CLI fails with 401 and "This API key is not bound to a user", which is the most common false alarm during a migration.
Bind the new key to a dedicated, non-personal user. A v4 key inherits its reach from the bound user's live claims. Bound to a manager, it is silently narrowed to that manager's teams and regions; bound to someone who later leaves, it stops working the moment their account is disabled.
Scope mapping
v2 gates each endpoint on its own scope string, around thirty of them. v4 gates on ten capabilities: a read and a write for each of employees, orders, inventory, teams and company.
Collapsing the first set into the second is a security change, not a rename. One v4 capability can cover several v2 scopes, so a key translated row-by-row usually ends up broader than the key it replaces. Grant only the rows your integration actually calls.
The starkest case is the delivery address: three separate v2 scopes collapse into the two employee capabilities, so a key that could previously only edit an address can now edit the whole employee record.
Employees
| v2 scope | v2 endpoint | v4 capability |
|---|---|---|
api.employees.getAll | GET /employees | mcp:employees:read |
api.employee.get | GET /employees/:id | mcp:employees:read |
api.employees.create | POST /employees | mcp:employees:write |
api.employee.update | PATCH /employees/:id | mcp:employees:write |
api.employee.products.get | GET /employees/:id/products | mcp:inventory:read |
api.employee.products.create | POST /employees/products/productInUse | mcp:inventory:write |
Note the two crossings into inventory: reading an employee's assigned devices and registering an in-use product are inventory operations in v4, even though v2 filed them under employees. A key that only reads employee records no longer gets their devices for free.
Delivery address
| v2 scope | v2 endpoint | v4 capability |
|---|---|---|
api.employee.deliveryAddress.get | GET /employees/:id/deliveryAddress | mcp:employees:read |
api.employee.deliveryAddress.create | POST /employees/:id/deliveryAddress | mcp:employees:write |
api.employee.deliveryAddress.update | PATCH /employees/:id/deliveryAddress | mcp:employees:write |
The address is not a sub-resource in v4. It is a set of fields on the employee, read with GET /v4/employees/{id} and written with PATCH /v4/employees/{id}.
Teams
| v2 scope | v2 endpoint | v4 capability |
|---|---|---|
api.teams.getAll | GET /teams | mcp:teams:read |
api.team.get | GET /teams/:teamId | mcp:teams:read |
api.teams.create | POST /teams | mcp:teams:write |
api.team.update | PATCH /teams/:teamId | mcp:teams:write |
Orders
| v2 scope | v2 endpoint | v4 capability |
|---|---|---|
api.orders.getAll | GET /orders | mcp:orders:read |
api.orders.create | POST /orders/assignToEmployee | mcp:orders:write |
api.orders.create | POST /orders/offboarding | mcp:orders:write |
api.orders.create | POST /orders/collection | mcp:orders:write |
api.orders.create | POST /orders/swap | mcp:orders:write |
api.orders.create | POST /orders/purchaseOrder | mcp:orders:write |
Products
| v2 scope | v2 endpoint | v4 capability |
|---|---|---|
api.products.getAll | GET /products | mcp:inventory:read |
api.product.get | GET /products/:productId | mcp:inventory:read |
api.products.create | POST /products/uploadProducts | mcp:inventory:write |
api.product.update | POST /products/addPinCode | mcp:inventory:write |
Products are inventory in v4. There is no mcp:products:* pair, because splitting them would put reading one device and searching your inventory behind different capabilities.
Company
| v2 scope | v2 endpoint | v4 capability |
|---|---|---|
api.company.addresses.get | GET /companyAddresses | mcp:company:read |
api.company.addresses.create | POST /companyAddresses | mcp:company:write |
Not migrated yet
These v2 scopes have no v4 successor. They stay valid against v2, so a client that uses them keeps its v2 key for those calls while moving everything else. The cutover is per endpoint group, not per API version.
| v2 scope | Why |
|---|---|
api.catalogs.getAll, api.catalogs.items.getAll | Catalogs and collections are outside the migrated resource groups |
api.company.get, api.company.update | No v4 company read or update operation |
api.orders.update | No v4 order-update operation |
api.employee.offBoarding | Offboarding is an order type in v4, covered by mcp:orders:write |
api.organization.* | The Organization API authenticates an organization across many companies; v4 has no organization-level principal |
What changes in your code
Lists paginate, and truncate at 50
v2 list endpoints return a bare JSON array with everything in it. v4 returns an envelope with page, pageSize, totalCount and nextCursor, and caps a page at 50 records.
This is the change most likely to break a migrated client quietly: it truncates rather than errors, so a job that used to process every employee silently starts processing the first 50. Page with page and limit, and check totalCount.
One order endpoint instead of four
assignToEmployee, offboarding, collection and swap all become a single POST /v4/orders, discriminated by an orderType field (Deployment, Offboarding, Collection, Swap from Inventory). You switch from picking a URL to setting a field.
purchaseOrder is the exception: it maps to POST /v4/orders/purchase, because it buys from the catalog rather than moving inventory you already own.
Renamed and reshaped fields
| v2 | v4 | Note |
|---|---|---|
secondaryEmail | personalEmail | Same stored field, renamed at the edge. Passing null or "" still clears it |
teamId | teamName | v4 addresses teams by name, creating the team on demand |
deliveryAddress.* sub-resource | Fields on the employee | Read and written through the employee endpoints |
deliveryAddress.country (required) | country (optional) | v4 is looser here |
v4 also adds fields v2 never had (department, division, jobTitle, taxId), plus a guard on PATCH /v4/employees/{id} that rejects an email change for an employee who already has an active login.
Capabilities are not hierarchical
mcp:orders:write does not imply mcp:orders:read. A client that places an order and then reads it back needs both. v2's per-endpoint scopes had the same property, but the coarser v4 names make it easy to assume otherwise.
Not yet available on v4
A few v2 behaviours have no v4 equivalent today. If you depend on one, keep that call on v2 for now:
- Filtering employees by
teamIdon the list endpoint ?invitation=trueon employee read, which creates and returns an invitation URLstartDateon employee creationaddressTwoandphoneon the delivery address at creation time
Reissue checklist
- Provision a dedicated, non-personal user in the company for the integration.
- Mint the key from the app's Developers → API Keys page as that user, selecting only the capabilities the mapping above calls for. The raw
grk_sk_key is shown once. - Send it as
X-API-KEY, notAuthorization: Bearer, which on a v4 route is only ever a Firebase ID token. - Verify with
GET /v4/me, which needs no capability and echoes back exactly what the key resolves to. - Update your calls for the contract changes above, pagination first. The Postman collection is the quickest way to rebuild and compare requests side by side: import it, paste your new key into the
apiKeyvariable, and work through the endpoints you use. - Keep the v2 key active until you have observed the new one working, then deactivate it.
If the integration you are moving is an AI assistant rather than a backend service, the same capabilities are reachable over MCP, and connect.growrk.com has the per-client setup steps for Claude, ChatGPT, Cursor and custom agents.
See Authentication & API keys for the full capability list and the v4 REST API for the endpoint reference.