Authentication
Three kinds of credential, one header. Which one you want depends on whether your own code is calling or a person is delegating to something else — and the difference is not cosmetic, because only one of them can be issued without us.
Three credentials
All three go in the same place — Authorization: Bearer … — and the service tells
them apart by prefix before it touches the database, so a malformed token costs a string
comparison rather than a query.
-
ptx_test_— an API key scoped to test mode. Reaches only test-mode datasets, never billed. -
ptx_live_— an API key scoped to live mode. Your own data, metered. -
ptx_oauth_— an OAuth access token, obtained by a person approving a connection. Always live.
Only the third is self-serve. API keys are issued by Plantactic against your account — how to ask — while an OAuth token is minted by the consent flow without anyone here being involved. That asymmetry is temporary and it is the current state, so plan around it rather than around the roadmap.
API keys
A key is a prefix followed by 256 bits of base64url. Send it whole.
curl https://ptx-api.plantactic.com/v1/datasets \
-H "Authorization: Bearer ptx_test_4e91c8..." You see it once. At issue time we return the secret and store only a SHA-256 hash of it, so a lost key is replaced rather than recovered — by us as much as by you. The hash is deliberately fast rather than a password hash: the secret is 256 random bits, so brute force is not on the table at any hash speed, and a slow hash would be paid on every authenticated request to buy nothing.
The first twelve characters are stored alongside the hash as a label, which is what lets support identify a key in a conversation without either of us handling the secret. Quote those when you write in; never the whole thing.
The two prefixes are never interchangeable and neither is any dataset created under one. Grep
for ptx_live_ in your repository and your CI logs — it is the cheapest control
available and the prefix exists to make it a one-line check.
The OAuth flow
Authorization code with PKCE, plus refresh. The authorization server is this API itself, so there is no second host to configure — ask it what it supports rather than hard-coding any of the below.
curl https://ptx-api.plantactic.com/.well-known/oauth-authorization-server {
"issuer": "https://ptx-api.plantactic.com",
"authorization_endpoint": "https://ptx-api.plantactic.com/oauth/authorize",
"token_endpoint": "https://ptx-api.plantactic.com/oauth/token",
"registration_endpoint": "https://ptx-api.plantactic.com/oauth/register",
"revocation_endpoint": "https://ptx-api.plantactic.com/oauth/revoke",
"scopes_supported": ["plantactic.read", "plantactic.write"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
} Four things in that document are worth reading twice.
-
code_challenge_methods_supportedis["S256"]and nothing else.plainis refused rather than tolerated. -
grant_types_supportedhas noclient_credentials. There is no machine-to-machine OAuth path here; that is what an API key is for. -
token_endpoint_auth_methods_supportedis["none"]— clients are public and prove themselves with PKCE, not a secret. -
registration_endpointis RFC 7591 dynamic client registration. A client enrols itself; nobody exchanges a client id with us by email.
Scopes are plantactic.read and plantactic.write. Approving a
connection grants both if both were asked for, and write is what allows creating datasets and
uploading contracts, so it is not a formality.
A token is bound to one API account. If the person approving has several, they choose during consent, and the choice is checked against their own — naming an account they are not linked to is refused rather than honoured. If they have none, one is created and linked at that moment, named from their email domain.
Authorization codes are single-use and live 60 seconds. Replaying one revokes the tokens already issued for it, on the assumption that a replayed code means someone else has a copy. Refresh tokens rotate on use.
What a 401 tells you
Three distinct causes, three distinct detail strings, one type. Branch
on the type; read the detail to find out what you did.
{
"type": "https://developer.plantactic.com/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Provide an API key as `Authorization: Bearer ptx_live_…`."
} {
"type": "https://developer.plantactic.com/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "That is not a Plantactic credential. API keys begin
`ptx_test_` or `ptx_live_`; OAuth access tokens begin
`ptx_oauth_`."
} {
"type": "https://developer.plantactic.com/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "That is a refresh token. Exchange it at /oauth/token for an
access token and present that instead."
}
That third one is worth calling out. Refresh tokens carry the prefix
ptx_oauth_rt_ and are explicitly refused as bearer credentials rather than merely
failing to match — presenting one is a common enough client bug that saying so beats a generic
rejection.
None of these carry an instance. They are produced by a filter before the request
reaches an endpoint, and only endpoints set that field —
Errors and retries covers the distinction.
Why none of this works in a browser
/v1 allows no browser origin. Not a narrow allowlist — none. A cross-origin call
from a page fails at the preflight, and that is deliberate: a key that a browser can send is a
key that has been published, because anything the browser can read a user can read.
So there is no client-side integration to build. Call this API from your backend and expose whatever your own front end needs, on your own terms.
Revoking
Revocation takes effect on the next request. Not after a cache expires — nothing here holds a verdict about a credential, and an earlier version of the connector that did was changed rather than tuned, because a revoked token that still answers for a minute is not revoked.
An OAuth grant is revoked at POST /oauth/revoke, or by disconnecting in the client
that holds it. An API key is revoked by asking us —
support@plantactic.com,
quoting the prefix rather than the key.
A revoked key and a key that never existed produce the same response, word for word. That is on purpose: answering "that one was revoked" confirms it was real, which is information the asker had not earned.