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.
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 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.
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.
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.
term the question in the reader’s own words. Every
word is matched on its own and the words a question is
phrased with are dropped, so a whole sentence finds what a
keyword would. Hits come back best first.locale is the reader’s language. Without one the answer is
in the workspace’s default language, and an article with no
copy in the language asked for answers its English one and
says so in locale.term reports the whole
set, which tells that apart from a workspace whose help
content has not arrived yet.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.
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.
application/vnd.api.v1+json is answered as one sending
version 2.info.version), whose minor part rises with each.406 and the
code API_VERSION_UNKNOWN.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.
POST /api/oauth/token, the OAuth 2.0
client credentials grant, so any OAuth client library can use
it without custom code.scope narrows a token to a subset of the permissions the
credential holds. Without it the token follows the
credential’s permissions on every request, so a right granted
in the application manager works on the next call.GET /api/me answers what the presented token allows, names
the switched-off groups under inactiveAccessGroups, and
names the workspace and its host, which is what a workflow
tool labels the connection with.POST /api/grant turns a scanned passport code into a child
credential for that one passport, valid for a day, so a
workshop or a recycler can record an event without an account
of its own.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.
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.
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.
brand_access - DELETE /brands/{id}, POST /brands, PUT /brands/{id}brand_write - DELETE /brands/{id}, POST /brands, PUT /brands/{id}component_access - DELETE /components/{id}, POST /components, PUT /components/{id}, PUT /components/{id}/publish, PUT /components/{id}/unpublishcomponent_write - DELETE /components/{id}, POST /components, PUT /components/{id}, PUT /components/{id}/publish, PUT /components/{id}/unpublishdpp_bulk_write - GET /dpps/bulk/{taskId}, POST /dpps/bulk, POST /dpps/bulk/validatedpp_dynamic - PATCH /dpps/{id}/dynamic_data, POST /grantdpp_events - POST /dpps/{id}/events, POST /grantdpp_history - GET /dpps/{id}/events, GET /events, POST /grant, POST /webhooks (beside webhook_access)dpp_lifecycle - POST /dpps/{id}/reissue, POST /dpps/{id}/supersede, POST /dpps/{id}/voiddpp_publish - POST /dpps/bulk (beside dpp_bulk_write), POST /dpps/{code}/publish (beside dpp_write), POST /dpps/{id}/correct (beside dpp_write)dpp_read - GET /dpps, GET /dpps/requirements, GET /dpps/{code}/private_dynamic_data, GET /dpps/{code}/private_properties, GET /dpps/{code}/private_properties/{version}, GET /dpps/{id}, GET /dpps/{id}/stats, GET /dpps/{id}/versions, GET /lots, GET /lots/{id}dpp_write - DELETE /dpps/{id}, POST /dpps, POST /dpps/validate, POST /dpps/{code}/publish, POST /dpps/{id}/correct, PUT /dpps/{id}export_access - GET /exports, GET /exports/{id}, GET /exports/{id}/download, POST /exportsimport_access - GET /imports, GET /imports/example, GET /imports/supplier_form, GET /imports/{id}, POST /imports, POST /imports/{id}/execute, POST /imports/{id}/revert, POST /imports/{id}/validate, PUT /imports/{id}/mappingsproduct_access - DELETE /products/{id}, GET /products/{id}, POST /products, PUT /products/{id}, PUT /products/{id}/mediafiles, PUT /products/{id}/publish, PUT /products/{id}/restore, PUT /products/{id}/unpublishtask_access - GET /tasks, GET /tasks/assignees, GET /tasks/{id}, GET /tasks/{id}/attachments/{attachmentId}, POST /tasks, POST /tasks/{id}/approve, POST /tasks/{id}/cancel, POST /tasks/{id}/claim, POST /tasks/{id}/complete, POST /tasks/{id}/reject, POST /tasks/{id}/release, POST /tasks/{id}/reopen, PUT /tasks/{id}webhook_access - DELETE /webhooks/{id}, GET /webhooks, GET /webhooks/{id}, POST /webhooks, POST /webhooks/{id}/regenerate_secret, POST /webhooks/{id}/test, PUT /webhooks/{id}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.
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.
sort=-updated_at lists the latest change first, and
sort=-created_at the newest record first. Without sort a
list keeps its own order. Ties fall to the id, so a page holds
the same records each time it is read.updated_since and created_since keep the records changed
or created from an ISO 8601 time on, that time included.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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.