# Métadonnées

Ajoutez vos propres références aux paiements, sessions, clients et remboursements, et retrouvez-les dans les webhooks et sur le tableau de bord.

Les **métadonnées** (*metadata*) sont un ensemble de clés et de valeurs à vous sur un objet : un
numéro de commande, un identifiant de panier, la boutique ou le canal d’où vient une vente.
AdProMaxPay les enregistre et les rend, inchangées, partout où l’objet apparaît. Elles ne changent
jamais le fonctionnement d’un paiement.

## Ajouter des métadonnées à un paiement

Envoyez `metadata`, un objet de chaînes, quand vous créez l’intention de paiement. Vos références
reviennent alors dans les réponses de l’API et dans chaque événement sur ce paiement : quand votre
webhook reçoit `payment_intent.succeeded`, `data.object.metadata.order_id` nomme la commande à
livrer.

```bash
curl https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1045-create" \
  -d '{
    "amount": 25000,
    "currency": "mad",
    "payment_method_types": ["card", "cash"],
    "metadata": { "order_id": "1045", "channel": "whatsapp" }
  }'
```

```js title="metadata.mjs"
const response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1045-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
    metadata: { order_id: '1045', channel: 'whatsapp' },
  }),
})
const intent = await response.json()
if (!response.ok)
  throw new Error(intent.error.message)
console.log(intent.id, intent.metadata.order_id)
```

```ts title="metadata.ts"
interface PaymentIntent {
  id: string
  object: 'payment_intent'
  amount: number
  currency: string
  status: string
  metadata: Record<string, string>
}

interface ApiError {
  error: { type: string, code: string, message: string, request_id: string }
}

const response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1045-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
    metadata: { order_id: '1045', channel: 'whatsapp' },
  }),
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  throw new Error(`${error.code}: ${error.message}`)
}
const intent = (await response.json()) as PaymentIntent
console.log(intent.id, intent.metadata.order_id)
```

```python title="metadata.py"
import os

import requests

response = requests.post(
    "https://pay-api.adpromax.com/v1/payment_intents",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    headers={"Idempotency-Key": "order-1045-create"},
    json={
        "amount": 25000,
        "currency": "mad",
        "payment_method_types": ["card", "cash"],
        "metadata": {"order_id": "1045", "channel": "whatsapp"},
    },
)
intent = response.json()
response.raise_for_status()
print(intent["id"], intent["metadata"]["order_id"])
```

```php title="metadata.php"
<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/payment_intents');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Idempotency-Key: order-1045-create',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 25000,
        'currency' => 'mad',
        'payment_method_types' => ['card', 'cash'],
        'metadata' => ['order_id' => '1045', 'channel' => 'whatsapp'],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$intent = json_decode(curl_exec($ch), true);
echo $intent['id'], ' ', $intent['metadata']['order_id'], PHP_EOL;
```

```ruby title="metadata.rb"
require 'json'
require 'net/http'

uri = URI('https://pay-api.adpromax.com/v1/payment_intents')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'order-1045-create'
request.body = JSON.generate(
  amount: 25000,
  currency: 'mad',
  payment_method_types: %w[card cash],
  metadata: { order_id: '1045', channel: 'whatsapp' }
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
intent = JSON.parse(response.body)
raise intent.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)

puts "#{intent['id']} #{intent['metadata']['order_id']}"
```

```go title="metadata.go"
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

type PaymentIntent struct {
	ID       string            `json:"id"`
	Status   string            `json:"status"`
	Metadata map[string]string `json:"metadata"`
}

type APIError struct {
	Error struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func main() {
	metadata := map[string]string{"order_id": "1045", "channel": "whatsapp"}
	body, err := json.Marshal(map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card", "cash"},
		"metadata":             metadata,
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/payment_intents", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", "order-1045-create")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		var apiErr APIError
		json.NewDecoder(res.Body).Decode(&apiErr)
		log.Fatal(apiErr.Error.Message)
	}
	var intent PaymentIntent
	if err := json.NewDecoder(res.Body).Decode(&intent); err != nil {
		log.Fatal(err)
	}
	fmt.Println(intent.ID, intent.Metadata["order_id"])
}
```

```java title="Metadata.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Metadata {
    public static void main(String[] args) throws Exception {
        // Your own references, as strings.
        String metadata = """
            { "order_id": "1045", "channel": "whatsapp" }""";
        String body = """
            {
              "amount": 25000,
              "currency": "mad",
              "payment_method_types": ["card", "cash"],
              "metadata": %s
            }
            """.formatted(metadata);
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/payment_intents"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", "order-1045-create")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(response.body());
        }
        // The payment intent as JSON: its metadata comes back as you sent it.
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

```csharp title="Metadata.cs"
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var json = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

using var request = new HttpRequestMessage(
    HttpMethod.Post, "https://pay-api.adpromax.com/v1/payment_intents");
request.Headers.Add("Idempotency-Key", "order-1045-create");
request.Content = JsonContent.Create(new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card", "cash" },
    metadata = new { order_id = "1045", channel = "whatsapp" },
});

using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
    var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
    throw new InvalidOperationException(failure?.Error.Message);
}
var intent = await response.Content.ReadFromJsonAsync<PaymentIntent>(json);
Console.WriteLine($"{intent!.Id} {intent.Metadata["order_id"]}");

record PaymentIntent(string Id, string Status, Dictionary<string, string> Metadata);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde = { version = "1", features = ["derive"] }
//             serde_json = "1"
use std::collections::HashMap;

use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct PaymentIntent {
    id: String,
    metadata: HashMap<String, String>,
}

#[derive(Deserialize)]
struct ApiError {
    error: ErrorBody,
}

#[derive(Deserialize)]
struct ErrorBody {
    message: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::blocking::Client::new()
        .post("https://pay-api.adpromax.com/v1/payment_intents")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", "order-1045-create")
        .json(&json!({
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card", "cash"],
            "metadata": { "order_id": "1045", "channel": "whatsapp" }
        }))
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    let intent: PaymentIntent = response.json()?;
    println!("{} {}", intent.id, intent.metadata["order_id"]);
    Ok(())
}
```

## Les limites

* Jusqu’à **50 clés** sur un objet.
* Une clé fait au plus **40 caractères** ; une valeur au plus **500 caractères**.
* Les valeurs sont des **chaînes** uniquement. Envoyez un nombre sous forme de texte : `"1045"`, pas
  `1045`.

Une requête qui dépasse ces limites est refusée avec `400`, `parameter_invalid`, et `param` nomme la
clé en cause, comme `metadata.order_id`. Rien n’est créé. Lisez `param` pour trouver la clé : le
`message` est une phrase générale et ne dit pas quelle limite est dépassée. Dans cette réponse, la
clé fait plus de 40 caractères.

```json title="400 Bad Request" response
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "param": "metadata.customer_reference_from_our_previous_system",
    "message": "This value is not valid.",
    "doc_url": "https://pay-docs.adpromax.com/errors#parameter_invalid",
    "request_id": "req_01k7e5s3d7f1g5h9j3k7m1n5pz"
  }
}
```

## Les objets qui prennent des métadonnées

| Objet                 | Les fixer                    | Les changer                                                                         |
| --------------------- | ---------------------------- | ----------------------------------------------------------------------------------- |
| Intention de paiement | `POST /v1/payment_intents`   | `POST /v1/payment_intents/{id}`, tant que le paiement est `requires_payment_method` |
| Session de paiement   | `POST /v1/checkout/sessions` | —                                                                                   |
| Client                | `POST /v1/customers`         | `POST /v1/customers/{id}`, à tout moment                                            |
| Remboursement         | `POST /v1/refunds`           | —                                                                                   |
| Endpoint de webhook   | `POST /v1/webhook_endpoints` | `POST /v1/webhook_endpoints/{id}`                                                   |

Une modification des métadonnées d’un endpoint de webhook est **fusionnée** avec ce qu’il contient :
les clés envoyées sont posées, les autres restent, et une clé envoyée avec une valeur vide (`""`)
est retirée.

## Les métadonnées restent là où vous les mettez

Les métadonnées appartiennent à l’objet sur lequel vous les fixez, et ne sont jamais copiées vers un
autre. Les `metadata` d’une session de paiement sont sur la session et dans ses événements
`checkout.session.*` ; l’intention de paiement que la session crée ne les porte pas.

Avec la page de paiement hébergée, conservez l’identifiant de l’intention de paiement avec votre
commande, comme le fait le [démarrage rapide carte](https://pay-docs.adpromax.com/fr/get-started/quickstart-card), ou lisez la
session quand `checkout.session.completed` arrive.

## Où vous les voyez

* Dans l’API : chaque réponse qui contient l’objet.
* Dans les webhooks : le `data.object` de l’événement porte les métadonnées de l’objet telles
  qu’elles étaient au moment de l’événement.
* Sur le tableau de bord : la page d’un paiement montre ses métadonnées, en lecture seule, et la liste
  des paiements peut être filtrée par métadonnées. Les métadonnées d’un client se modifient sur la
  page du client.

## Ce qui ne va jamais dans les métadonnées

Les métadonnées servent aux références, pas aux secrets. Toute votre équipe les voit sur le tableau
de bord, et vos endpoints de webhook les reçoivent.

* **Pas de numéros de carte.** Les numéros de carte ne transitent jamais par l’API : une valeur qui
  ressemble à un numéro de carte est refusée avec `parameter_invalid`.
* **Pas de mots de passe, de clés ni de jetons.**
* **Pas de données personnelles sensibles**, comme des numéros d’identité ou des informations de
  santé. Pour savoir qui a payé, utilisez un [client](https://pay-docs.adpromax.com/fr/payments/customers) et gardez le reste dans vos
  propres systèmes, sous votre identifiant.
