# Flux d'installation OAuth

Une application accède à une boutique par le flux OAuth 2.0 avec code d'autorisation et PKCE. Le marchand approuve votre application sur un écran de consentement DZBuild, votre serveur échange le code contre un jeton d'accès par boutique, et chaque jeton appelle l'API REST pour sa seule boutique.

## Endpoints[​](#endpoints "Lien direct vers Endpoints")

| Étape                | Requête                                                             | Qui l'envoie                                                            |
| -------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Autorisation         | `GET https://dzbuild.com/oauth/apps/authorize`                      | Le navigateur du marchand, envoyé par votre application                 |
| Approbation ou refus | `POST https://dzbuild.com/oauth/apps/approve` et `/oauth/apps/deny` | Le formulaire de consentement. Votre application ne les appelle jamais. |
| Échange du code      | `POST https://dzbuild.com/oauth/apps/token`                         | Votre serveur                                                           |
| Appels API           | `https://api.dzbuild.app/v1/...`                                    | Votre serveur, avec le jeton d'accès                                    |

## Le flux[​](#le-flux "Lien direct vers Le flux")

1. Votre serveur crée un vérificateur PKCE, son défi S256 et un `state` aléatoire.
2. Il envoie le marchand vers l'URL d'autorisation. Un marchand non connecté se connecte d'abord puis revient sur la même URL.
3. DZBuild affiche l'écran de consentement. Le marchand choisit les boutiques et approuve.
4. DZBuild redirige le navigateur vers votre `redirect_uri` avec un `code` et votre `state`.
5. Votre serveur envoie le code, le vérificateur et ses identifiants client à l'endpoint de jeton.
6. La réponse contient un jeton d'accès pour chaque boutique installée.

## Étape 1 : envoyer le marchand vers l'URL d'autorisation[​](#étape-1--envoyer-le-marchand-vers-lurl-dautorisation "Lien direct vers Étape 1 : envoyer le marchand vers l'URL d'autorisation")

| Paramètre               | Obligatoire | Règle                                                                                                                                                                                                          |
| ----------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `response_type`         | oui         | Toujours `code`.                                                                                                                                                                                               |
| `client_id`             | oui         | L'identifiant client de votre application, dans la console développeur : `dzapp_` suivi de 20 caractères hexadécimaux.                                                                                         |
| `redirect_uri`          | oui         | Une des adresses de redirection enregistrées sur l'application, caractère pour caractère. Aucune correspondance par préfixe ou par motif.                                                                      |
| `scope`                 | non         | Scopes séparés par des espaces. Chaque scope doit être enregistré sur l'application et autorisé pour les applications. Absent ou vide : tous les scopes enregistrés sur l'application. 512 caractères au plus. |
| `state`                 | oui         | 1 à 1024 caractères. DZBuild le renvoie sans modification. Liez-le à la session du marchand et vérifiez-le au retour.                                                                                          |
| `code_challenge`        | oui         | SHA-256 du vérificateur en base64url, sans remplissage : exactement 43 caractères parmi `A-Z a-z 0-9 - _`.                                                                                                     |
| `code_challenge_method` | oui         | Toujours `S256`. La méthode `plain` est refusée.                                                                                                                                                               |

Le vérificateur de code compte 43 à 128 caractères parmi `A-Z a-z 0-9 - . _ ~`. Gardez-le sur votre serveur jusqu'à l'échange du code. Il ne passe jamais par le navigateur.

PKCE en PHP :

```
<?php

function base64url(string $bytes): string

{

    return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');

}



$verifier = base64url(random_bytes(32));   // 43 characters

$challenge = base64url(hash('sha256', $verifier, true));



// Keep $verifier on your server (session or database) until the token call.

$authorizeUrl = 'https://dzbuild.com/oauth/apps/authorize?' . http_build_query([

    'response_type' => 'code',

    'client_id' => 'dzapp_0123456789abcdef0123',

    'redirect_uri' => 'https://app.example.com/dzbuild/callback',

    'scope' => 'orders:read products:read',

    'state' => bin2hex(random_bytes(16)),

    'code_challenge' => $challenge,

    'code_challenge_method' => 'S256',

], '', '&', PHP_QUERY_RFC3986);
```

PKCE en Node.js :

```
const crypto = require('node:crypto');



const verifier = crypto.randomBytes(32).toString('base64url'); // 43 characters

const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');



// Keep the verifier on your server (session or database) until the token call.

const authorizeUrl = 'https://dzbuild.com/oauth/apps/authorize?' + new URLSearchParams({

  response_type: 'code',

  client_id: 'dzapp_0123456789abcdef0123',

  redirect_uri: 'https://app.example.com/dzbuild/callback',

  scope: 'orders:read products:read',

  state: crypto.randomBytes(16).toString('hex'),

  code_challenge: challenge,

  code_challenge_method: 'S256',

});
```

Pour tester votre propre code : le vérificateur d'exemple de la RFC 7636, `dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk`, doit donner le défi `E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM`.

## Étape 2 : l'écran de consentement[​](#étape-2--lécran-de-consentement "Lien direct vers Étape 2 : l'écran de consentement")

L'écran de consentement affiche le nom, le logo et le développeur de votre application. Une application approuvée porte un badge « vérifiée par DZBuild ». Une application pas encore approuvée affiche un avis de mode test, et seul son développeur peut l'ouvrir.

Le marchand voit les boutiques dont il est propriétaire. Les membres d'équipe ne voient aucune boutique, car une installation donne un jeton pour toute la boutique. Une boutique qui ne peut pas recevoir l'application est affichée avec la raison : le marchand n'en est pas propriétaire, l'application est en mode test, le plan de la boutique est inférieur au plan minimum de l'application, ou l'application est suspendue. Le marchand peut approuver jusqu'à 10 boutiques à la fois.

Les scopes demandés sont regroupés par ressource. La demande de consentement reste valable 10 minutes.

## Étape 3 : traiter la redirection[​](#étape-3--traiter-la-redirection "Lien direct vers Étape 3 : traiter la redirection")

Après l'approbation, DZBuild répond `302` vers votre adresse de redirection :

```
HTTP/1.1 302 Found

Location: https://app.example.com/dzbuild/callback?code=3f9a...64-hex...&state=c2a8a879c9434b775191caf5b17d846f
```

Le code fait 64 caractères hexadécimaux, reste valable 10 minutes et ne sert qu'une fois. Vérifiez que `state` correspond à la valeur enregistrée avant d'utiliser le code. Si votre adresse de redirection enregistrée contient déjà une chaîne de requête, DZBuild ajoute ses paramètres avec `&`.

### Erreurs d'autorisation et de consentement[​](#erreurs-dautorisation-et-de-consentement "Lien direct vers Erreurs d'autorisation et de consentement")

Les erreurs vous arrivent de deux façons. Tant que DZBuild n'a pas reconnu votre `client_id` et votre `redirect_uri`, il affiche une page d'erreur au marchand et ne redirige jamais : un lien erroné ne peut pas envoyer de code vers une adresse inconnue. Ensuite, il redirige vers votre `redirect_uri` avec un paramètre `error`. Les contrôles s'exécutent dans l'ordre de ce tableau.

| Condition                                                                                                              | Résultat                          | Renvoyé comme                                      |
| ---------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------- |
| `client_id` inconnu ou mal formé, ou `redirect_uri` non enregistrée à l'identique                                      | Page d'erreur, HTTP 400           | Page pour le marchand                              |
| Application non approuvée et le marchand n'est pas son développeur, ou application refusée ou suspendue                | Page d'erreur, HTTP 404           | Page pour le marchand                              |
| `response_type` différent de `code`                                                                                    | `error=unsupported_response_type` | Redirection, avec `state` quand `state` est valide |
| `state` absent ou plus long que 1024 caractères                                                                        | `error=invalid_request`           | Redirection, sans `state`                          |
| `code_challenge` différent de 43 caractères base64url, ou `code_challenge_method` différent de `S256`                  | `error=invalid_request`           | Redirection, avec `state`                          |
| Un scope non enregistré sur l'application, non autorisé pour les applications, ou `scope` plus long que 512 caractères | `error=invalid_scope`             | Redirection, avec `state`                          |
| Le marchand clique sur Refuser                                                                                         | `error=access_denied`             | Redirection, avec `state`                          |
| Aucune boutique choisie                                                                                                | Page d'erreur, HTTP 400           | Page pour le marchand                              |
| Plus de 10 boutiques choisies                                                                                          | Page d'erreur, HTTP 400           | Page pour le marchand                              |
| Une boutique choisie n'appartient pas au marchand ou ne peut pas recevoir l'application                                | Page d'erreur, HTTP 403           | Page pour le marchand                              |
| Demande de consentement expirée ou déjà traitée                                                                        | Page d'erreur, HTTP 400           | Page pour le marchand                              |

## Étape 4 : échanger le code[​](#étape-4--échanger-le-code "Lien direct vers Étape 4 : échanger le code")

Votre serveur envoie un formulaire (`application/x-www-form-urlencoded`) à l'endpoint de jeton.

| Champ           | Valeur                                                             |
| --------------- | ------------------------------------------------------------------ |
| `grant_type`    | `authorization_code`                                               |
| `code`          | Le code reçu dans la redirection.                                  |
| `redirect_uri`  | La même chaîne que celle envoyée à l'URL d'autorisation.           |
| `code_verifier` | Le vérificateur derrière votre `code_challenge`.                   |
| `client_id`     | Votre identifiant client.                                          |
| `client_secret` | Votre client secret (`dzas_` suivi de 48 caractères hexadécimaux). |

Vous pouvez envoyer `client_id` et `client_secret` en identifiants HTTP Basic au lieu de champs de formulaire. DZBuild lit d'abord les champs du formulaire et n'utilise l'en-tête `Authorization: Basic` que si aucun des deux champs n'est présent. Dans Basic, encodez les deux valeurs en form-urlencoded comme l'exige la section 2.3.1 de la RFC 6749.

Envoyez un en-tête `User-Agent` sur cette requête et sur chaque appel à l'API, par exemple `my-app/1.0 (+https://example.com)`. `dzbuild.com` répond à un `POST` sans cet en-tête, y compris cette requête de jeton, par un `403` et une page de vérification HTML au lieu de JSON. Le `fetch` de Cloudflare Workers et plusieurs bibliothèques HTTP n'envoient pas de `User-Agent` si vous n'en fixez pas un ; `curl` envoie le sien.

```
curl -sS https://dzbuild.com/oauth/apps/token \

  -H "Accept: application/json" \

  --data-urlencode "grant_type=authorization_code" \

  --data-urlencode "code=$CODE" \

  --data-urlencode "redirect_uri=https://app.example.com/dzbuild/callback" \

  --data-urlencode "code_verifier=$CODE_VERIFIER" \

  --data-urlencode "client_id=$DZBUILD_CLIENT_ID" \

  --data-urlencode "client_secret=$DZBUILD_CLIENT_SECRET"
```

Un échange réussi répond `200` avec `Cache-Control: no-store` :

```
{

  "access_token": "dzpk_live_...",

  "token_type": "Bearer",

  "scope": "orders:read products:read",

  "store_id": 141,

  "install_id": 57,

  "stores": [

    {

      "store_id": 141,

      "store_name": "Boutique Amel",

      "install_id": 57,

      "access_token": "dzpk_live_..."

    },

    {

      "store_id": 152,

      "store_name": "Amel Kids",

      "install_id": 58,

      "access_token": "dzpk_live_..."

    }

  ]

}
```

| Champ                    | Signification                                                                                        |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| `access_token`           | Le jeton de la première entrée de `stores`.                                                          |
| `token_type`             | Toujours `Bearer`.                                                                                   |
| `scope`                  | Les scopes accordés, séparés par des espaces. Chaque boutique de la réponse reçoit les mêmes scopes. |
| `store_id`, `install_id` | La première entrée de `stores`.                                                                      |
| `stores`                 | Une entrée par boutique installée : `store_id`, `store_name`, `install_id`, `access_token`.          |

La réponse ne contient ni `expires_in` ni `refresh_token`. Conservez chaque jeton côté serveur, indexé par `store_id`. Ce jeton d'accès par boutique s'appelle le jeton d'installation dans les autres pages.

### Erreurs d'échange[​](#erreurs-déchange "Lien direct vers Erreurs d'échange")

Chaque erreur est un JSON avec `error` et `error_description`, et `Cache-Control: no-store`.

| HTTP | `error`                  | `error_description`                                                          | Cause                                                                                                                                                                             |
| ---- | ------------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 401  | `invalid_client`         | `Client authentication failed`                                               | `client_id` ou `client_secret` absent ou faux. Avec des identifiants Basic, la réponse porte aussi `WWW-Authenticate: Basic realm="dzbuild"`.                                     |
| 400  | `unsupported_grant_type` | `Only authorization_code is supported`                                       | `grant_type` différent de `authorization_code`.                                                                                                                                   |
| 400  | `invalid_grant`          | `The code is invalid, expired, already used, or does not match this request` | Code inconnu, expiré ou déjà utilisé, code émis pour une autre application, `redirect_uri` différente, ou vérificateur qui ne correspond pas au défi.                             |
| 400  | `invalid_grant`          | `No approved store could be installed`                                       | Aucune boutique choisie n'a pu être installée : chacune a échoué au contrôle final (par exemple un plan inférieur au plan minimum de l'application) ou son installation a échoué. |
| 500  | `server_error`           | `The code could not be checked` ou `The install could not be completed`      | Une panne de la plateforme. Renvoyez le marchand vers l'étape d'autorisation.                                                                                                     |

DZBuild vérifie d'abord le client, puis `grant_type`, puis consomme le code. Un code consommé est perdu même quand le `redirect_uri` ou le vérificateur est faux : un échange raté ne peut pas être rejoué avec le même code. Lancez une nouvelle demande d'autorisation.

## Installations sur plusieurs boutiques[​](#installations-sur-plusieurs-boutiques "Lien direct vers Installations sur plusieurs boutiques")

Un marchand propriétaire de plusieurs boutiques peut en approuver jusqu'à 10 en un seul consentement. La réponse de l'échange liste chaque boutique installée dans `stores`, avec son propre `install_id` et son propre `access_token`. Un jeton n'atteint que sa boutique.

DZBuild contrôle chaque boutique une nouvelle fois pendant l'échange. Une boutique qui ne remplit plus les conditions est écartée, donc `stores` peut contenir moins de boutiques que le marchand n'en a cochées. Lisez `stores`, pas le consentement que vous attendiez.

Relancer le flux pour une boutique qui a déjà votre application met à jour la même installation : l'`install_id` reste, les scopes deviennent ceux du nouveau consentement, et un nouveau jeton remplace l'ancien. L'ancien jeton cesse de fonctionner immédiatement.

## Appeler l'API[​](#appeler-lapi "Lien direct vers Appeler l'API")

Envoyez le jeton dans un en-tête Bearer vers `https://api.dzbuild.app/v1`. `GET /v1/whoami` ne demande aucun scope et montre ce qu'un jeton peut faire :

```
curl -sS https://api.dzbuild.app/v1/whoami \

  -H "Authorization: Bearer $DZBUILD_ACCESS_TOKEN"
```

```
{

  "data": {

    "key_id": "...",

    "store_id": 141,

    "type": "platform",

    "rate_limit_tier": "enterprise",

    "pilot": true,

    "scopes": ["orders:read", "products:read"],

    "app": {

      "app_id": 12,

      "client_id": "dzapp_0123456789abcdef0123",

      "install_id": 57

    }

  },

  "meta": {

    "request_id": "...",

    "api_version": "v1"

  }

}
```

Les jetons d'application fonctionnent avec tous les plans de boutique. Le niveau `enterprise` affiché par `whoami` est l'étiquette que l'API donne aux jetons d'application ; les limites propres à votre application sont sur la page [limites de requêtes](https://dzbuild.dev/fr/fr/rate-limits.md).

## Durée de vie et révocation du jeton[​](#durée-de-vie-et-révocation-du-jeton "Lien direct vers Durée de vie et révocation du jeton")

Un jeton d'accès n'a pas de date d'expiration. Il fonctionne jusqu'à l'un de ces événements :

* Le marchand désinstalle l'application. Le jeton est révoqué et les appels répondent `401` avec le code `unauthorized` et le message `Invalid or revoked API key`. Un endpoint webhook vérifié reçoit un seul événement `app.uninstalled`.
* Vous relancez le flux d'installation pour la même boutique. Le nouveau jeton remplace l'ancien.

Tant que l'installation existe, l'API refuse le jeton avec `403` dans ces cas :

| `error.code`        | Message                                                            | Quand                                                                                                                                 |
| ------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `app_uninstalled`   | `This app is no longer installed on this store`                    | L'installation n'est plus active.                                                                                                     |
| `app_suspended`     | `This app has been suspended by DZBuild`                           | DZBuild a suspendu ou refusé l'application. Les appels refonctionnent quand la suspension est levée.                                  |
| `app_not_approved`  | `This app is in test mode and only runs on its developer's stores` | L'application n'a encore jamais été approuvée (brouillon ou première vérification) et la boutique n'appartient pas à son développeur. |
| `app_plan_required` | `This app requires the ... plan`                                   | Le plan de la boutique, ou un plan payant expiré, est inférieur au plan minimum de l'application. Le message nomme le plan.           |

Via `api.dzbuild.app`, la passerelle transmet la réponse de DZBuild telle quelle : les codes `403` ci-dessus, un `401` ou un `429` vous arrivent avec le statut et le corps décrits ici. Si la passerelle ne peut pas vérifier le jeton auprès de DZBuild, parce que DZBuild est injoignable ou répond par une erreur serveur, elle répond `502` avec le code `server_error` et le message `Key lookup failed, retry shortly`. Réessayez après un court délai.

Il n'existe pas d'endpoint de révocation pour les applications dans la v1. La désinstallation se fait par le marchand, depuis la page de l'application dans son tableau de bord.

Renouveler le client secret dans la console développeur le remplace immédiatement. Les jetons déjà émis continuent de fonctionner.

## Limites de requêtes[​](#limites-de-requêtes "Lien direct vers Limites de requêtes")

| Endpoint                             | Limite                                                                               |
| ------------------------------------ | ------------------------------------------------------------------------------------ |
| `GET /oauth/apps/authorize`          | 30 requêtes par 300 secondes                                                         |
| `POST /oauth/apps/approve`           | 10 requêtes par 600 secondes                                                         |
| `POST /oauth/apps/token`             | 60 requêtes par 300 secondes                                                         |
| API REST avec un jeton d'application | 120 requêtes par minute et par installation, avant la limite partagée de la boutique |

Les limites OAuth se comptent par client, identifié par son adresse IP, les en-têtes du navigateur et la session. Restez en dessous : un client qui dépasse une limite peut recevoir `429` avec un en-tête `Retry-After` de 120 secondes au plus. Envoyez `Accept: application/json` sur les appels d'échange pour que le refus revienne en JSON et non en redirection. Les limites de l'API sont sur la page [limites de requêtes](https://dzbuild.dev/fr/fr/rate-limits.md).
