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

FieldTypeRequiredDescription
connectionstringYesThe connection value from the redirect after the vendor allowed your app. Valid for 60 minutes; at most 64 characters
labelstringYesThe 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

CodeStatusDescription
BRANCH_INACTIVE422The 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_REACHED422The 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_EXPIRED422The handle is older than 60 minutes. Send the vendor through the connect flow again
CONNECTION_NOT_FOUND404No 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

FieldTypeRequiredDescription
client_secretstringYesThe 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

CodeStatusDescription
CLIENT_NOT_PARTNER_MADE403The vendor made this client in the admin portal. Only the vendor can change its secret there
INVALID_CLIENT_SECRET401The 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

FieldTypeRequiredDescription
client_secretstringYesThe 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

CodeStatusDescription
CLIENT_NOT_PARTNER_MADE403The vendor made this client in the admin portal. Only the vendor can revoke it there
INVALID_CLIENT_SECRET401The 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