> ## Documentation Index
> Fetch the complete documentation index at: https://docs-api.maketou.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Utilisez l'API Maketou v1 décrite par la spécification OpenAPI de ce site.
> Gardez la clé API côté serveur et ne l'exposez jamais dans du code client, des logs ou une réponse destinée au navigateur.
> Ne livrez un produit et n'activez un accès que lorsque le statut du panier est `completed`.
> N'inventez pas d'endpoint, de champ ou de webhook absent de la documentation.

# Webhooks de panier

> Recevez une notification sur votre serveur lorsque le paiement d'un panier aboutit ou échoue.

Au lieu d'interroger régulièrement l'API pour connaître le statut d'un panier, vous pouvez fournir une `webhookUrl` lors de sa création. Maketou envoie alors une requête `POST` vers cette URL lorsque le statut du panier change.

## Activer le webhook

Ajoutez le champ `webhookUrl` dans le body de la [création de panier](/guides/create-cart.md) :

```json theme={null}
{
  "productDocumentId": "550e8400-e29b-41d4-a716-446655440000",
  "email": "client@example.com",
  "firstName": "Jean",
  "lastName": "Dupont",
  "redirectURL": "https://monsite.com/confirmation",
  "webhookUrl": "https://monsite.com/webhooks/maketou-cart"
}
```

L'URL doit être absolue et utiliser le protocole `http` ou `https`. Utilisez une URL `https` accessible publiquement.

La `webhookUrl` est propre à chaque panier. Vous pouvez donc utiliser une URL différente selon le panier, par exemple pour transmettre un identifiant dans la query string.

## Événements notifiés

| Statut du panier | Quand |
| - | - |
| `completed` | Le paiement est confirmé. Pour un produit gratuit, la notification est envoyée dès la création du panier |
| `payment_failed` | Le paiement a échoué |

Un panier en `payment_failed` peut ensuite passer en `completed`, par exemple lorsque le prestataire de paiement confirme le paiement en retard. Vous recevez alors une nouvelle notification avec le statut `completed`.

Les paniers `abandoned` ne sont pas notifiés.

## Body de la requête

Le body est identique à la réponse de [Vérifier un panier](/guides/retrieve-cart.md) (`GET /api/v1/stores/cart/{cartId}`) :

```http theme={null}
POST /webhooks/maketou-cart HTTP/1.1
Content-Type: application/json
X-Cart-Signature: t=1760004300,v1=ZmFrZS1zaWduYXR1cmUtZXhhbXBsZQ==
```

```json theme={null}
{
  "id": "fd2d91d7-20d2-4b86-b067-d474fb0d1e60",
  "createdAt": "2025-11-28T14:27:00.805Z",
  "updatedAt": "2025-11-28T14:29:12.341Z",
  "paymentId": "4d729bf5-ef1e-4f7f-bf10-e63bd3db9b04",
  "status": "completed",
  "customerInfo": {
    "lang": "fr-FR",
    "email": "customer@example.com",
    "phone": "+33612345678",
    "lastName": "Doe",
    "firstName": "John"
  },
  "meta": {
    "userId": "12345"
  }
}
```

Le body reflète l'état du panier au moment de l'envoi. Utilisez le champ `status` pour décider de l'action à mener, et `meta` pour retrouver la commande correspondante dans votre système (voir [Métadonnées](/guides/metadata.md)).

## Répondre à la notification

Répondez avec un code HTTP `2xx` dès réception. Traitez la notification de manière asynchrone si votre traitement est long.

Si votre serveur ne répond pas en `2xx`, Maketou renvoie la notification jusqu'à 2 fois, après environ 30 secondes puis 60 secondes. Au-delà, la notification est abandonnée. Vous pouvez toujours récupérer le statut avec [Vérifier un panier](/guides/retrieve-cart.md).

> [!WARNING]
>
> Vous pouvez recevoir plusieurs notifications pour un même panier. Rendez votre traitement idempotent : ne livrez le produit qu'une seule fois, même si vous recevez `completed` plusieurs fois.

## Vérifier la signature

Chaque notification est signée par Maketou. Vérifiez la signature avant de traiter la notification, pour vous assurer qu'elle provient bien de Maketou et que son contenu n'a pas été modifié.

### Header de signature

```
X-Cart-Signature: t=1760004300,v1=ZmFrZS1zaWduYXR1cmUtZXhhbXBsZQ==
```

* `t` : timestamp Unix (en secondes) au moment de l'envoi.
* `v1` : signature Ed25519, encodée en base64.

### Récupérer la clé publique

```http theme={null}
GET https://api.maketou.net/api/v1/stores/cart/webhook-signing-key
```

Cet endpoint est public et ne nécessite pas de clé API. La clé est la même pour toutes les boutiques : vous pouvez la récupérer une fois et la stocker dans votre configuration.

```bash theme={null}
curl https://api.maketou.net/api/v1/stores/cart/webhook-signing-key
```

```json theme={null}
{
  "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n",
  "algorithm": "ed25519",
  "header": "X-Cart-Signature: t=<unix timestamp>,v1=<base64 signature>. Signed string: '<t>.<canonical JSON of the payload>' (object keys sorted, no whitespace)."
}
```

### Reconstruire le message signé

Le message signé est `<t>.<JSON canonique du body>` :

* `<t>` est la valeur `t` du header ;
* le JSON canonique est le body sérialisé avec les clés de chaque objet triées par ordre alphabétique, sans espaces ni retours à la ligne.

Parsez le body JSON reçu, puis sérialisez-le de nouveau en JSON canonique. Ne signez pas le body brut tel que reçu : son formatage peut différer.

### Exemple en Node.js

```js theme={null}
import crypto from 'node:crypto';

function canonicalJson(value) {
  if (value === null || typeof value !== 'object') {
    return JSON.stringify(value);
  }

  if (Array.isArray(value)) {
    return `[${value.map(canonicalJson).join(',')}]`;
  }

  const entries = Object.entries(value)
    .filter(([, entryValue]) => entryValue !== undefined)
    .sort(([left], [right]) => left.localeCompare(right));

  return `{${entries
    .map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`)
    .join(',')}}`;
}

export function verifyCartWebhook(body, signatureHeader, publicKeyPem) {
  const timestamp = signatureHeader.match(/t=(\d+)/)?.[1];
  const signature = signatureHeader.match(/v1=([^,]+)/)?.[1];

  if (!timestamp || !signature) {
    return false;
  }

  const ageInSeconds = Math.floor(Date.now() / 1000) - Number(timestamp);

  if (ageInSeconds > 300) {
    return false;
  }

  return crypto.verify(
    null,
    Buffer.from(`${timestamp}.${canonicalJson(body)}`),
    crypto.createPublicKey(publicKeyPem),
    Buffer.from(signature, 'base64'),
  );
}
```

Ici, `body` est le body JSON déjà parsé, par exemple `req.body` avec Express et `express.json()`.

> [!NOTE]
>
> Le timestamp `t` fait partie du message signé. Rejetez les notifications trop anciennes (par exemple plus de 5 minutes) pour vous protéger contre le rejeu d'une requête interceptée.

## Bonnes pratiques

* **Vérifiez toujours la signature** avant de traiter une notification.
* **Ne livrez le produit que sur `completed`**, comme pour la vérification par l'API.
* **Rendez votre traitement idempotent** : un même panier peut être notifié plusieurs fois.
* **Gardez une solution de repli** : si vous n'avez pas reçu de notification, vérifiez le statut avec [Vérifier un panier](/guides/retrieve-cart.md), en respectant les [limites de fréquence](/resources/rate-limits.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.