Nordic Wear API DocumentationReference

API guide

One page to get a token, learn what a credential unlocks, check a passport before writing it, and see which calls cannot be undone. The full reference, every field of every endpoint, is the API documentation.

Start here

Exchange the client id and secret for a token, then ask what the token allows:

curl -X POST https://<host>/api/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=<client id> \
  -d client_secret=<client secret>
transpareo auth login --host <host> --client-id <client id>

curl https://<host>/api/me -H "Authorization: Bearer <token>"
transpareo me

The five calls most integrations need:

# 1. What a passport of this product would need
curl "https://<host>/api/dpps/requirements?productId=<id>&granularity=item" \
  -H "Authorization: Bearer <token>"
transpareo dpps requirements --product-id <id> --granularity item

# 2. Check a passport without writing anything
curl -X POST https://<host>/api/dpps/validate \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{"dpp":{"unlockableId":<id>,"granularity":"item","batchIdentifier":"<lot>","serialIdentifier":"000412"}}'
transpareo dpps validate --file passport.json

# 3. Write it
curl -X POST https://<host>/api/dpps \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -H "Idempotency-Key: create-000412" \
  -d '{"dpp":{"unlockableId":<id>,"granularity":"item","batchIdentifier":"<lot>","serialIdentifier":"000412"}}'
transpareo dpps create --file passport.json

# 4. Publish it
curl -X POST https://<host>/api/dpps/<code>/publish \
  -H "Authorization: Bearer <token>"
transpareo dpps publish <code>

# 5. Follow what happened
curl "https://<host>/api/events?since=<cursor>" \
  -H "Authorization: Bearer <token>"
transpareo events tail --since <cursor>

The command line

The same API answers a terminal, a script and an assistant through transpareo, the open source command line client and MCP server. Install it with the script on Linux or macOS, or with Homebrew on macOS. The rest is one login, and for an assistant one registration:

curl -fsSL https://transpareo.com/cli/install.sh | sh
brew install transpareo/tap/transpareo

transpareo auth login --host <workspace host> --client-id <key>
transpareo me
transpareo setup claude

The login reads the secret from standard input or from TRANSPAREO_CLIENT_SECRET, never from an option, and keeps it in the operating system’s keyring with the token beside it, so later commands reuse the token for its hour. Every endpoint is a command; transpareo api <METHOD> <path> reaches one that has no command of its own.

The setup line installs the agent skill and registers the MCP server, whose configuration holds no secret, only the profile name:

{ "mcpServers": { "transpareo": { "command": "transpareo",
  "args": ["mcp", "--profile", "acme"] } } }

The repository at https://github.com/transpareo/transpareo-cli documents every command with its example, the MCP tools and the credential store.

Assistants

Every workspace answers the Model Context Protocol at https://<host>/mcp, so an assistant running in a browser reaches the same tools with nothing installed. ChatGPT and Claude connect by that address alone: paste it as a connector, sign in to the workspace, and approve the consent screen, which names every permission the assistant asks for one by one. The OpenAI Agents SDK’s hosted tool takes the same address with a token instead. The tools are the ones the command line client offers, under the same names. The connection appears on the API consumers page of the application manager, with the name and the date of the person who approved it, and is switched off there.

Help

This guide describes the calls a program makes. The help articles describe the screens a person uses: where a setting lives, what a button does, which step comes before which. They are the same articles the application manager shows, written for the operator and translated into every language the platform carries.

GET /api/help searches them and GET /api/help/{key} reads one; as tools they are search_help and read_help. Both need a valid token and no permission key.

The base URL

Every endpoint lives under /api on the workspace’s own host. The version rides in a header, described under Versions, so no address carries it.

Versions

The API is at version 2. A request names its version in the Accept header, application/vnd.api.v2+json, or in the v parameter, ?v=2. A request that names none is answered by the current version.

Credentials

A credential is a machine account with its own permissions, rate limits and audit trail. It is created in the application manager, which shows the secret once.

An assistant or a third-party application asks a person for access instead of holding a credential of its own. It registers itself at POST /api/oauth/register, sends the person to GET /api/oauth/authorize with a PKCE challenge, and the person approves the request in the application manager, where the requested permissions are named one by one. The result is a credential of its own, owned by that person, that can never reach further than they can, and that stops working when they lose access or revoke it on the API consumers page. Tokens obtained this way behave like any other: one hour, bound to this host, narrowed by scope. A refresh token keeps the connection alive without asking again and rotates on every use, and POST /api/oauth/revoke ends either token on request.

Suspending or revoking a credential, and regenerating its secret, invalidates every token already issued to it.

The storefront is the other kind of caller: a person signs in at POST /api/session and the answer carries a token for that session. Everything below is about machine credentials.

What a credential may see and change

A credential holds what the grid of its access groups says, as a person does, and the records it creates belong to it. A group grants each resource at View only or Full access: either reads, and creating, changing and importing take Full access. A credential changes and deletes the products, components and brands the group’s reach gives it to write: under Own and team’s the records it created and those of the group’s members, under All every record, under Selected the records picked. Full access with All on components or brands is what the workspace-wide write keys component_write and brand_write stand for. A credential holding dpp_write changes every passport it reads. A credential acting for a person changes what that person changes in the application manager, and what it created itself. A person signing in sees their own records and those of their group; an administrator sees all of them.

The product and component lists of a credential carry every record its access groups reach, drafts included, and the brand list every brand of the workspace; rejected products are left out. A term searches within the same reach, and published=false selects the drafts and published=true the published records. The storefront’s own readers keep the published lists.

On a workspace whose storefront shows no lists, a list the credential holds no key for answers 403, and the message names the key, so an empty list always means there is nothing to see. Where the storefront shows its lists, such a credential reads the published records, as the storefront’s readers do.

A single product, component or brand that a credential holding none of its keys may not read answers 403 naming the key, for a record that exists and one that does not alike. A credential holding the key, at either tier, reads by id the records its list shows, drafts included, and gets 404 for a draft its access groups or its scope leave out, as for one that does not exist. A write refused because the group holds the resource’s line at View only names the line and Full access in the message and the hint.

A write naming a property type that does not exist, a product property type only the members of a plan edit, or one of an access group the credential is not in, is refused whole with 422 PRODUCT_PROPERTIES_REFUSED or COMPONENT_PROPERTIES_REFUSED. Nothing is stored, and the message names each such type and why. GET /products/new lists the product property types a credential may write. A passport whose propertyTypeIds names a type that does not exist is refused the same way, with DPP_PROPERTY_TYPES_REFUSED, and one sending a single id where the list goes with DPP_KEYS_REFUSED.

A property type of the input type grade takes a rating class, such as an EU energy label class, from its scale, which GET /products/new lists under allowedValues, best first. Send the class as the scale spells it. A value outside the scale is refused with 422 and PRODUCT_PROPERTIES_NOT_IN_SCALE, and the message names the type, the value and the classes. A class is a plain value and never feeds a rating.

Publication is the storefront’s concept: it says whether the catalogue shows a record. Read GET /me for workspace.publishesOnCreate before you treat publishing as a step.

Where it is true, the workspace has no storefront. Every product and every component is published as it is created, the publish and unpublish operations answer 403, and the screens the person works on carry no publish control, so ask them to publish nothing and leave a storefront, catalogue visibility and a frontend app out of what you tell them.

Where it is false, a record is published on demand. Publish a component before products can carry it, and publish a product to make it visible. Publication decides one link either way: a product’s component rows name their components, and a name links to a component only while that component is published.

A credential issued for a scanned passport code carries a scope: a list of passports it is confined to. Anything outside that list answers as though it did not exist.

Permissions

A permission grants a verb. A call the credential has no permission for is refused, and the answer says which key it would need.

The keys are read off the access group grid. A line grants its resource key (product_access, task_access and so on) at the line’s level. DPPs at View only grants dpp_read and dpp_history; at Full access also dpp_write, dpp_bulk_write, dpp_lifecycle, dpp_events and dpp_dynamic. The action lines under DPPs grant on their own: Publish DPPs grants dpp_publish, Record events (repair, recall, recycling) dpp_events and Update changing values (e.g. battery health) dpp_dynamic, so a credential can add to a passport it reads without changing it.

Pagination

Lists take page and per_page. The answer carries the total and the page size in headers, and a Link header with rel="next" while a further page exists. Follow that header rather than counting pages.

Sorting and polling

The lists a workflow tool polls take the same three parameters besides their own filters: GET /api/products, /api/components, /api/dpps, /api/lots and /api/tasks.

Every record of these lists carries createdAt and updatedAt in whole seconds, and updatedAt moves with every save.

To poll, note the time before each call and send the time noted before the previous one as updated_since, with sort=-updated_at. Follow the Link header through the pages of one call. A record that changes while you page moves to the top, so a later page may repeat one you have read, and the record at the bound comes back on the next poll: skip a record whose id and updatedAt you already hold. A value a list cannot read answers 422, SORT_INVALID or SINCE_FILTER_INVALID.

Repeating a request safely

Send an Idempotency-Key header with a creating request. The same key and body within a day replays the stored answer and carries Idempotent-Replayed: true. The same key with a different body is refused. A key whose first request is still running answers 409, so wait and retry.

Bulk passport creation deduplicates differently: rows are matched by the identifiers that name the unit, so resubmitting a batch after a timeout creates nothing twice.

Writing a passport

Two shapes come up. Both start by asking rather than guessing.

A product that does not exist yet. GET /api/products/new answers the property types the workspace defines, which of them are mandatory, their units and their allowed values. Fill those in and POST /api/products. A product belongs to a brand, named beside it under brand, and is made of components, so the body names at least one under componentsInput, a list of names or of objects naming a component. A product missing a mandatory property is refused, and the answer names the properties under fields.properties.missing. The product’s identifiers go in beside its name: gtin, the manufacturer part number under mpn where it has no GTIN, and the customs tariff number under hsCode. Its weight goes under weight with its unit under weightUnit, and a component’s share under percentage in its entry.

The write stores every field it takes and refuses everything else with 422 PRODUCT_KEYS_REFUSED, one entry under fields per key: a key it does not know, also inside a component entry or beside product, and a list or an object sent as something else. The product’s read may come back with the write and passes as it was read; a key of it that changed names what writes it instead, such as categoryIds for categories, brand.name for brandName and PUT /api/products/{id}/publish for published, or says that no call writes it.

A variant of a product. Name the product under parentId in POST /api/products, with a name of its own. The variant starts as a copy of its parent and takes the parent’s later changes. Whatever the body names besides, and whatever a later update changes, becomes the variant’s own; a value sent back as it was read goes on following. The variant’s GET /api/products/{id} marks each value follows, pinned or parent_moved, the last where the parent changed a value the variant holds of its own. PUT /api/products/{id}/restore has a value follow the parent again.

Passports for a product that exists. GET /api/dpps/requirements answers which identifiers the chosen granularity needs, which properties the passport inherits, what a publish would say about it today, and a body ready to fill. Its missing list names what the product still lacks, one entry per open check with the product field that answers it and, under operation, the call that takes the field, and ready whether anything required is left; a workflow loops over the list, asks whoever holds each value, and comes back when the answers are stored. Then POST /api/dpps/validate to see the result without writing, POST /api/dpps to write, and the publish endpoint to seal it.

Validation is worth the extra call: it runs the same check a publish runs, so a passport that validates is one that publishes.

The passport lifecycle

A passport is a draft until it is published. Publishing signs a snapshot and registers a version; every later change publishes a new version, and the old ones stay readable. A draft takes the product as it stands at every validate and every publish, and the copy freezes when the passport is published. A unit that leaves circulation is voided; a unit replaced by another is superseded, naming its successor; a passport reissued for the same unit records that too. Each of those is an event on the passport’s own log.

Calls that cannot be undone

Publishing is irreversible by design: the signed snapshot goes into the ten-year archive and its URL may already be printed on a label. These calls cannot be undone either:

Work that runs in the background

Bulk creation, exports and imports answer 202 with the address to poll. Poll it until the status is final, then read the result from the same document. Nothing is lost if the polling stops; the work continues.

A run whose id the caller no longer holds is found again in the listings. GET /api/imports and GET /api/exports answer the caller’s own runs, newest first, each as a summary naming the statusUrl the whole run is read from.

The event feed

GET /api/events answers every event of every passport in the order the events were recorded. Keep the nextCursor of each answer and send it as since on the next call. An empty answer between polls is normal, and the cursor comes back unchanged.

Events younger than five seconds are held back, so nothing can appear behind a cursor already handed out. types and dppCode narrow the feed.

Webhooks

A credential registers its own endpoints under /api/webhooks and receives the same events as a delivery: subscribing needs webhook_access at Full access (the line Webhooks) and dpp_history, listing and testing take View only, and a delivery carries only the events the feed answers that credential, of the passports it reaches. While the credential is suspended, revoked or expired, nothing is delivered. Each delivery carries X-Transpareo-Signature as t=<unix time>,v1=<hex>, an HMAC-SHA256 over <t>.<body> with the subscription’s secret. Verify it by recomputing the digest over the timestamp, a full stop and the raw body, comparing in constant time, and rejecting a timestamp too far from now.

POST /api/webhooks/{id}/test delivers a synthetic ping and answers what the endpoint did, so a subscription can be checked before it is relied on. After eight failed deliveries in a row a subscription switches itself off. An endpoint that answers a delivery with 410 Gone ends its subscription at once: it is switched off, the event is not retried, and goneAt says when. Switching a subscription back on clears the count and goneAt.

Tasks

A task is something one person asks of another, assigned to one person or to one access group. A member claims a group task before working on it. A task ends done or cancelled and stays on record. The calls live under /api/tasks and need task_access, the line Tasks: View only reads the lists and makes the moves, asking someone for something takes Full access. Only a person or a group holding the line is given a task.

A credential that acts for a person, a grant the person approved or their assistant, works with tasks as that person: it reads their lists, claims and finishes tasks and asks others in their name. A credential an operator set up by hand acts as itself: it asks people for things and looks after what it asked for, and it claims and finishes nothing, since only a person works on a task.

GET /api/tasks reads one list: mine (the default) for what waits on the person, the unclaimed tasks of their groups included, waiting for what they wait on others for, requested for the open tasks the caller asked of others, done for the finished and cancelled ones it took part in, and all for every task it takes part in, every task for an administrator. A list the caller may not read answers 422 and names the ones it may.

Each task names in moves what the caller may do with it now. Whoever the task is for, or a member of its group, claims and completes it; completing an unclaimed group task claims it in the same call. Whoever holds a task, whoever asked for it, or an administrator releases it. Whoever asked for it, or an administrator, changes, cancels and reopens it. A move the task refuses answers 422 with the reason in the code, such as TASK_ALREADY_CLAIMED.

POST /api/tasks takes an assigneeKey, User:<id> or AccessGroup:<id>. GET /api/tasks/assignees?term= finds the key by name.

Rate limits

The hourly allowance belongs to the credential, not to the address, so every location using the same credentials shares it. Every answer carries X-RateLimit-Limit, -Remaining and -Reset, the Unix time at which the current window ends. Bulk rows count against a separate per-minute allowance. A refusal answers 429 with Retry-After in seconds; wait that long rather than retrying at once.

The token exchange and the sign-in attempts are throttled separately, per address, to bound guessing.

Caching

A request carrying a token is answered fresh every time. The storefront’s anonymous reads are cached for ten minutes by default, and the Cache-Control header of each answer says what applies to it. Append ?reload=1 to such a read to go past the cache when a fresh answer matters more than a fast one.

When something is refused

Every error has the same shape: a stable error code, a message in the workspace’s language, fields when individual attributes failed, and a hint saying what to do next. The hint is the part worth reading first; it names the permission to ask for, the URL to poll, or the option that overrides the refusal.

Every error also carries docsUrl, the address of the section of this guide that explains the rule the request ran into.

Reading after writing

A write answers the record it wrote, so there is nothing to wait for on the API itself. The public passport page is served through a content delivery network: publishing purges the document that lists the versions, and every snapshot URL names the bytes it holds, so a reader never sees old content under a fresh address.