API reference
Partner endpoints
This endpoint is for a partner: a company whose app (a till, a webshop) connects to Puntjes for many vendors. It takes a different token from every other endpoint. Read Partner integrations for the whole flow before you call it.
Create an API client from a connection
POST /api/v1/partner/clients
Send the connection handle from the redirect after the vendor allowed your app, with the access
token you got for the same code. The answer is a normal API client of that vendor: from then
on your app uses client_id and client_secret with the client credentials grant, as every
integration does.
The token must come from the authorization code flow, for your partner client and for the user who
allowed the connection. A client credentials token answers 401 INVALID_CLIENT. The partner token
works on this endpoint only: every vendor endpoint answers it with 401 INVALID_CLIENT.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
connection | string | Yes | The connection value from the redirect after the vendor allowed your app. Valid for 60 minutes; at most 64 characters |
label | string | Yes | The name of the new client, as the vendor sees it under API clients. At most 100 characters |
Example request
{
"connection": "3vX9kQ2mP7rT4wY8zB1nC6dF0gH5jL2sKaEuWiOo",
"label": "Kassa 1"
}
curl -X POST https://puntjes.app/api/v1/partner/clients \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"connection":"3vX9kQ2mP7rT4wY8zB1nC6dF0gH5jL2sKaEuWiOo","label":"Kassa 1"}'
branch is the default branch the vendor picked when it allowed your app, or null when it picked
none. Your client can still name any branch of the vendor on a request; the default is only what a
request with no branch uses.
Response (201 Created)
{
"data": {
"client_id": "9f3c2a6e-4b1d-4c8e-9a7f-2d5e8b1c0a43",
"client_secret": "YOUR_NEW_CLIENT_SECRET",
"vendor": {
"id": 7,
"name": "Bakkerij Peeters"
},
"branch": {
"external_id": "kerkstraat",
"name": "Kerkstraat",
"type": "physical"
}
}
}
Retry with the same handle
Send the same connection again while it is valid (60 minutes after the vendor allowed your
app) and you get the same client_id and client_secret. No second client is made, also when
two calls arrive at the same time. A new label on a retry is ignored. Store the secret when it
arrives: once the handle expires, Puntjes does not give it to you again.
A retry still checks the vendor. A vendor that is suspended in the meantime answers its 403
code, not the secret, and a client the vendor revoked answers CONNECTION_NOT_FOUND.
Errors
| Code | Status | Description |
|---|---|---|
BRANCH_INACTIVE | 422 | The branch the vendor picked is closed. The vendor opens it again, then you retry with the same handle; or the vendor connects again |
CLIENT_LIMIT_REACHED | 422 | The vendor has as many live API clients as it may have. The vendor revokes one, then you retry with the same handle before it expires |
CONNECTION_EXPIRED | 422 | The handle is older than 60 minutes. Send the vendor through the connect flow again |
CONNECTION_NOT_FOUND | 404 | No connection matches this handle for your token: an unknown handle, one made for another partner or user, a user who may no longer allow it, or a client the vendor revoked. Send the vendor through the connect flow again |
The next two endpoints are for your app at the vendor: the till or the desktop app that collected an API
client with POST /partner/clients. They act on that API client. Send a token that this client got with
the client credentials grant, not the partner token of the connect flow, and put the current
client_secret of the client in the JSON body. A token alone changes nothing: a stolen token must not turn
into a new secret or a deleted client. Puntjes reads the secret from the body only, never from the query
string, because a URL goes into access logs.
Both endpoints work for a client that a partner made, and answer 403 CLIENT_NOT_PARTNER_MADE for a client
that the vendor made in the portal. They also work when the vendor's subscription has ended and the rest of
the API is read only. A vendor that the API refuses (pending, deactivated, or suspended after its grace days)
gets its 403 code. Each client can make 10 calls a minute to the two endpoints together. Puntjes writes
each change and each delete to the audit log of the vendor, with your partner as the subject.
Change the secret of a client
POST /api/v1/client/secret
Call this endpoint when the secret of the client may have leaked, for example when somebody copied the storage of your app.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
client_secret | string | Yes | The current client_secret of the client of the token, or the old one in its 60 minutes after a change. At most 255 characters |
Example request
{
"client_secret": "YOUR_CLIENT_SECRET"
}
curl -X POST https://puntjes.app/api/v1/client/secret \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"client_secret":"YOUR_CLIENT_SECRET"}'
Response
{
"data": {
"client_id": "9f3c2a6e-4b1d-4c8e-9a7f-2d5e8b1c0a43",
"client_secret": "YOUR_CLIENT_SECRET"
}
}
The answer holds the new client_secret, and Puntjes shows it this one time. client_id does not
change. Every access token of the client stops at once, also the token you sent with this call. Store the
new secret, then get a new token with it at once: that first token ends the old secret. Until then, a
person who has only the old secret can still use the client.
The old secret works for 60 minutes
After the change, the old secret still gets a token at POST /oauth/token for 60 minutes, so an app
that has not stored the new secret yet keeps working. The old secret and each token that it got stop at
once when the client first gets a token with the new secret. Puntjes keeps one old secret only: the next
change replaces it.
Retry when the answer is lost
Without the answer you do not have the new secret. Inside the 60 minutes, get a token with the old secret and send this call again with the old secret in the body. You get a fresh secret, and the secret of the lost answer stops working. The old secret keeps its first deadline: a retry does not give it 60 more minutes.
Errors
| Code | Status | Description |
|---|---|---|
CLIENT_NOT_PARTNER_MADE | 403 | The vendor made this client in the admin portal. Only the vendor can change its secret there |
INVALID_CLIENT_SECRET | 401 | The client_secret is not the current secret of the client, and not the old secret in its 60 minutes after a change. Nothing changed |
Delete a client
DELETE /api/v1/client
Call this endpoint when your app is reinstalled, moves to another vendor account, or goes out of use. It revokes the client and every access token of it, and frees its place under the vendor's limit of API clients. Without this call, every reinstall leaves a live client behind, and the vendor reaches its limit. Inside the 60 minutes after a change of the secret, the old secret also works here.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
client_secret | string | Yes | The current client_secret of the client of the token, or the old one in its 60 minutes after a change. At most 255 characters |
Example request
{
"client_secret": "YOUR_CLIENT_SECRET"
}
curl -X DELETE https://puntjes.app/api/v1/client \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"client_secret":"YOUR_CLIENT_SECRET"}'
Response
204 No Content on success. From then on the client gets no token.
Errors
| Code | Status | Description |
|---|---|---|
CLIENT_NOT_PARTNER_MADE | 403 | The vendor made this client in the admin portal. Only the vendor can revoke it there |
INVALID_CLIENT_SECRET | 401 | The client_secret is not the current secret of the client, and not the old secret in its 60 minutes after a change. Nothing was deleted |