Handleidingen

Partnerintegraties

Een partner is een bedrijf met een app (een kassa, een webshop) die voor veel handelaars met Puntjes koppelt. Zonder deze flow maakt elke handelaar een API-client in het portaal en kopieert hij het secret naar je app. Met deze flow klikt de handelaar één keer op Toestaan in Puntjes, en je app haalt zelf een API-client van die handelaar op.

De flow is OAuth2 authorization code met PKCE, gevolgd door één aanroep van de partner-API. Daarna is je app een gewone integratie van de handelaar en gebruikt ze de client credentials grant, zoals Authenticatie uitlegt.


Wat je van Puntjes krijgt

Puntjes registreert je app als partner en geeft je:

  • een partnerclient-ID;
  • voor een confidential client (een server die een geheim kan bewaren) een client secret, één keer getoond. Bewaar het meteen: Puntjes houdt alleen een hash bij;
  • voor een public client (een app op een toestel dat geen geheim kan bewaren) helemaal geen secret.

Je geeft Puntjes 1 tot 10 terugstuuradressen: volledige https-adressen zonder * en zonder #. De redirect_uri die je later stuurt, moet daar letter voor letter een van zijn. Een public client kan ook een loopback-adres opgeven, zoals Een desktop-app uitlegt.

Je kunt Puntjes ook een koppeladres geven: een volledig https-adres van de pagina op je site waar een handelaar de koppeling start. De partnerpagina in het Puntjes-portaal krijgt dan een knop Koppelen die de beheerder van de handelaar daarheen stuurt. Zonder koppeladres opent de knop je website.

De partnerclient kan de API van een handelaar niet gebruiken. Hij start alleen koppelingen.


1. Stuur de handelaar naar Puntjes

Open deze URL in de browser van de handelaar:

GET https://puntjes.app/oauth/authorize
    ?response_type=code
    &client_id=JOUW_PARTNERCLIENT_ID
    &redirect_uri=https://app.example.com/puntjes/callback
    &state=JOUW_STATE
    &code_challenge=JOUW_CODE_CHALLENGE
    &code_challenge_method=S256
    &branch=kerkstraat
ParameterVerplichtBeschrijving
response_typeJaAltijd code
client_idJaJe partnerclient-ID
redirect_uriJaEen van je geregistreerde terugstuuradressen, exact. Hoogstens 2000 tekens
stateJaEen willekeurige waarde die je voor deze browser bijhoudt. Puntjes stuurt ze ongewijzigd terug. Hoogstens 500 tekens
code_challengeJaBASE64URL(SHA256(code_verifier)), zonder = op het einde. Hoogstens 128 tekens
code_challenge_methodJaAltijd S256
branchNeeDe external_id van een vestiging van de handelaar. Puntjes kiest die alvast op het toestemmingsscherm. Hoogstens 64 tekens

De code_verifier is een willekeurige reeks van 43 tot 128 tekens uit A-Z, a-z, 0-9, -, ., _ en ~. Bewaar hem bij de state: je stuurt hem in stap 3.

Stuur altijd code_challenge_method=S256 mee

Ontbreekt code_challenge_method, dan neemt de OAuth-server plain, en dat weigert Puntjes. Een request zonder methode of met plain komt terug op je terugstuuradres met error=invalid_request, net als een request zonder code_challenge van een confidential client. Een public client zonder code_challenge, een code_challenge die niet uit 43 tot 128 tekens uit A-Z, a-z, 0-9, -, ., _ en ~ bestaat, of een andere methode dan S256 en plain krijgt een fout in de browser (400 invalid_request) en wordt niet teruggestuurd.

Stuur geen scope mee: Puntjes kent geen scopes, en een request met een scope komt terug met error=invalid_scope. Stuur ook geen prompt=none: de handelaar moet het scherm zien, en Puntjes antwoordt met error=consent_required.

Een request met een onbekend client-ID, een terugstuuradres dat niet geregistreerd is of een parameter boven zijn limiet vertrouwt Puntjes niet. Dan toont Puntjes een fout in de browser en stuurt het niet terug, want het weet niet zeker dat het adres van jou is.

De handelaar logt in als dat nodig is en ziet de naam van je app op het toestemmingsscherm. Hij kiest een standaardvestiging, of geen, en klikt op Toestaan of Weigeren. Een branch die je meestuurt, kiest die vestiging alleen alvast: de handelaar kan een andere kiezen. Past de sleutel bij geen actieve vestiging, dan negeert Puntjes hem en staat de eerste actieve vestiging gekozen. Alleen een beheerder van de handelaar die de API-clients mag beheren, kan toestaan.


2. Lees de redirect

Na Toestaan komt de browser terug op je redirect_uri met drie parameters:

https://app.example.com/puntjes/callback?code=…&state=JOUW_STATE&connection=…
ParameterBeschrijving
codeDe authorization code voor stap 3
stateJe state, ongewijzigd. Vergelijk ze met de waarde die je bijhield, en stop als ze verschilt
connectionHet kenmerk van deze koppeling, voor stap 4. Het werkt 60 minuten

Heeft je terugstuuradres al een querystring, dan blijft die staan; Puntjes zet de drie parameters erachter.

Na Weigeren komt de browser terug met error=access_denied en je state, en Puntjes bewaart niets.

Hou deze URL uit je logs

De redirect-URL bevat de code en het kenmerk connection. Samen met een token leveren die een API-client van de handelaar op. Schrijf de volledige URL niet weg in een log, een analysetool of een foutrapport.


3. Ruil de code in voor een token

Stuur de code snel naar het token-endpoint: een authorization code werkt 10 minuten en maar één keer.

curl -X POST https://puntjes.app/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d client_id=JOUW_PARTNERCLIENT_ID \
  -d client_secret=JOUW_PARTNERCLIENT_SECRET \
  -d redirect_uri=https://app.example.com/puntjes/callback \
  -d code=DE_CODE \
  -d code_verifier=JOUW_CODE_VERIFIER

redirect_uri is dezelfde waarde als in stap 1. Een confidential client stuurt zijn client_secret mee; een public client laat het weg. Het antwoord bevat een access_token dat 60 minuten werkt. Er is geen refresh token: het token dient alleen voor stap 4, en de volgende koppeling begint weer bij stap 1.


4. Haal de API-client op

curl -X POST https://puntjes.app/api/v1/partner/clients \
  -H "Authorization: Bearer JOUW_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"connection":"HET_KENMERK","label":"Kassa 1"}'

label is de naam die de handelaar voor deze client in het portaal ziet, hoogstens 100 tekens. Het antwoord is 201 met de nieuwe client:

{
    "data": {
        "client_id": "9f3c2a6e-4b1d-4c8e-9a7f-2d5e8b1c0a43",
        "client_secret": "JOUW_NIEUWE_CLIENT_SECRET",
        "vendor": { "id": 7, "name": "Bakkerij Peeters" },
        "branch": { "external_id": "kerkstraat", "name": "Kerkstraat", "type": "physical" }
    }
}

branch is de standaardvestiging die de handelaar koos, of null. Bewaar client_secret meteen.

Opnieuw proberen is veilig. Stuur dezelfde connection opnieuw binnen haar 60 minuten en je krijgt dezelfde client_id en client_secret, ook als twee aanroepen tegelijk binnenkomen. Puntjes maakt één client per koppeling. Na 60 minuten antwoordt het kenmerk CONNECTION_EXPIRED, en CONNECTION_NOT_FOUND zodra de nachtelijke opruiming de koppeling heeft gewist. In beide gevallen komt het secret niet meer mee.

De pagina Partner-endpoints heeft de volledige referentie.

Als de aanroep mislukt

Sommige weigeringen kan alleen de handelaar oplossen. Toon de handelaar wat hij moet doen, en probeer daarna opnieuw met hetzelfde kenmerk zolang het geldig is.

CodeStatusWie lost het op
CLIENT_LIMIT_REACHED422De handelaar: hij trekt een API-client in in het portaal. Daarna probeer je opnieuw met hetzelfde kenmerk
BRANCH_INACTIVE422De handelaar: hij opent de gekozen vestiging opnieuw en jij probeert opnieuw; of hij koppelt opnieuw
VENDOR_PENDING, VENDOR_SUSPENDED, VENDOR_DEACTIVATED, VENDOR_INACTIVE403De handelaar, samen met Puntjes: zijn account mag de API niet gebruiken
SUBSCRIPTION_EXPIRED403De handelaar: hij kiest een plan onder Facturatie
CONNECTION_EXPIRED422Niemand: het kenmerk is ouder dan 60 minuten. De handelaar koppelt opnieuw
CONNECTION_NOT_FOUND404Niemand: de handelaar koppelt opnieuw
VALIDATION_ERROR422Jij: connection of label ontbreekt of is te lang
UNAUTHENTICATED401Jij: het token ontbreekt of is verlopen, of je toegang als partner is ingetrokken
INVALID_CLIENT401Jij: het token is geen partnertoken uit stap 3, bv. een token uit client credentials

CONNECTION_NOT_FOUND geeft bewust één antwoord voor verschillende oorzaken: een onbekend kenmerk, een kenmerk van een andere partner, een kenmerk dat een andere gebruiker toestond dan de gebruiker van het token, een gebruiker die geen beheerder van de handelaar meer is, een client die de handelaar introk nadat je hem ophaalde, of een verlopen kenmerk na de nachtelijke opruiming. Een fout kenmerk verraadt dus niet welke controle mislukte.


Een desktop-app

Een desktop-app kan geen geheim bewaren, en ze kan geen redirect op een https-adres ontvangen. Ze gebruikt daarom een listener op dezelfde computer (RFC 8252, sectie 7.3). Doe dit:

  • Laat Puntjes je app registreren als public client met een loopback-terugstuuradres: http://127.0.0.1/puntjes/callback of http://[::1]/puntjes/callback. Laat de poort weg. In stap 1 mag je dan elke poort sturen, en het pad moet hetzelfde blijven. In productie weigert Puntjes http://localhost, omdat een naam naar een ander adres kan wijzen.
  • Luister alleen op het loopback-adres dat je registreerde (127.0.0.1 of [::1]), nooit op alle netwerkadressen van de computer.
  • Open voor stap 1 de standaardbrowser van het systeem, geen webweergave in je app (RFC 8252, sectie 8.12).
  • Neem één request aan op de listener. Antwoord met een pagina op een adres zonder de query, zodat de code uit de adresbalk verdwijnt. Stop daarna de listener.
  • Stuur in stap 3 geen client_secret.
  • Stuur in stap 3 dezelfde redirect_uri als in stap 1, met dezelfde poort. Een andere poort geeft 400 invalid_request.
  • Hou de redirect-URL uit je logs: hij bevat de code en het kenmerk connection.

Limieten

WatLimiet
POST /api/v1/partner/clients30 aanroepen per minuut per partnerclient; 30 per minuut per adres zonder geldig token
POST /api/v1/client/secret en DELETE /api/v1/client10 aanroepen per minuut per client voor de twee samen; 10 per minuut per adres zonder geldig token
GET /oauth/authorize30 requests per minuut per browseradres, of per aangemelde gebruiker
POST /oauth/token60 requests per minuut per client-ID vanaf één IP-adres; 600 per minuut per IP-adres
Authorization code10 minuten, één keer bruikbaar
Access token uit de code60 minuten, geen refresh token
Kenmerk connection60 minuten

Boven een limiet antwoordt de partner-API 429 RATE_LIMITED.


Na de koppeling

De client die je ophaalde, is een gewone API-client van de handelaar. Vraag een token met de client credentials grant (zie Authenticatie) en roep daarmee de API van de handelaar aan. De client mag de hele zaak gebruiken: elke vestiging, elke klant. De vestiging uit de koppeling is alleen de standaard voor een request zonder branch.

De handelaar ziet de client in het portaal onder API-clients, met je label, en kan hem daar intrekken.

Je app kan zonder de handelaar iets met de client doen. Is het secret misschien uitgelekt, dan vraagt ze een nieuw met POST /api/v1/client/secret. Wordt de app opnieuw geïnstalleerd of niet meer gebruikt, dan verwijdert ze de client met DELETE /api/v1/client, zodat oude clients het maximum aantal API-clients van de handelaar niet opvullen. Beide aanroepen vragen een token van de client en zijn client_secret; Partner-endpoints geeft de details.