Vue d’ensemble
Une interface personnalisée est facultative. Ajoutez-en une si les utilisateurs de votre plugin doivent examiner, comparer, modifier ou confirmer des informations structurées, ou naviguer parmi elles. Veillez à ce que les outils MCP restent utiles sans composant pour que ChatGPT et Codex puissent mener le workflow à son terme sans interface.
Le serveur MCP renvoie des ressources d’interface pour certains outils. Dans ChatGPT, les composants s’exécutent dans une iframe, communiquent avec l’hôte via la passerelle MCP Apps (JSON-RPC via postMessage) et s’affichent aux côtés de la conversation. Le standard ouvert MCP Apps permet à l’interface de fonctionner sur différents hôtes compatibles.
Commencez par MCP Apps
ChatGPT implémente le standard ouvert MCP Apps pour les interfaces renvoyées par un serveur MCP. MCP Apps définit comment votre serveur associe les outils aux ressources d’interface et comment l’iframe communique avec son hôte.
Pour une nouvelle interface :
- Déclarez la ressource d’interface avec
_meta.ui.resourceUri. - Utilisez la passerelle JSON-RPC
ui/*viapostMessagepour l’initialisation, les notifications, les appels d’outils, les messages et le contexte accessible au modèle. - Veillez à ce que les outils restent utiles sans interface pour que le modèle puisse mener le workflow à son terme dans les clients qui n’affichent pas les composants.
En vous appuyant sur ce standard, vous pouvez utiliser la même interface dans ChatGPT et sur d’autres hôtes compatibles avec MCP Apps.
Pour mettre en œuvre le standard, consultez la spécification MCP Apps.
Pour les applications en barre latérale, les visionneuses de fichiers et les formulaires plus élaborés, consultez la page Extensions.
Ajoutez les extensions ChatGPT
Une fois que votre intégration MCP Apps fonctionne, réservez window.openai aux fonctionnalités que la spécification commune ne couvre pas. Ces extensions facultatives peuvent améliorer l’expérience dans ChatGPT tout en préservant la portabilité de l’interface de base.
Privilégiez les champs et les méthodes du standard
Lorsque la spécification commune couvre une fonctionnalité, utilisez le champ ou la méthode MCP Apps correspondant :
| Objectif | Standard MCP Apps | Alias de compatibilité ChatGPT |
|---|---|---|
| Associer un outil à une ressource d’interface | _meta.ui.resourceUri |
_meta["openai/outputTemplate"] |
| Recevoir les données d’entrée d’un outil | ui/initialize + ui/notifications/tool-input |
window.openai.toolInput |
| Recevoir les résultats d’un outil | ui/notifications/tool-result |
window.openai.toolOutput |
| Appeler un outil depuis l’interface | tools/call |
window.openai.callTool |
| Envoyer un message de suivi | ui/message |
window.openai.sendFollowUpMessage |
Les alias de compatibilité restent disponibles pour les intégrations existantes. Pour toute nouvelle interface, utilisez les champs du standard et les méthodes de la passerelle indiqués dans la colonne du milieu.
Voici quelques exemples d’extensions :
- Le paiement instantané avec
window.openai.requestCheckout. - La gestion des fichiers dans ChatGPT avec
window.openai.uploadFile,window.openai.selectFilesetwindow.openai.getFileDownloadUrl. - Les fenêtres modales gérées par l’hôte avec
window.openai.requestModal. - La persistance de l’état du widget avec
window.openai.widgetStateetwindow.openai.setWidgetState.
Vérifiez la disponibilité de chaque extension et prévoyez une solution de repli lorsque c’est possible :
const openai = typeof window !== "undefined" ? window.openai : undefined;
if (openai?.requestModal) {
await openai.requestModal({
template: "ui://widget/details.html",
});
} else {
document.body.textContent = "This host does not support modals.";
}Évitez de conditionner le comportement de l’interface au nom de l’hôte ou du produit. Vérifiez directement la disponibilité de la fonctionnalité dont l’interface a besoin.
Pour connaître les signatures des extensions et voir des exemples, consultez la référence de la passerelle de composants window.openai.
Bibliothèque de composants OpenAI facultative
La bibliothèque de composants @openai/apps-sdk-ui fournit des boutons, des cartes, des champs de saisie et des primitives de mise en page prêts à l’emploi, adaptés au conteneur de ChatGPT. Utilisez-la pour obtenir un style cohérent sans recréer les composants de base.
Vous pouvez également explorer le dépôt d’exemples d’interfaces sur GitHub.
Choisissez un mode de présentation
Commencez par une interface intégrée à la conversation et ne demandez plus d’espace que si le workflow l’exige. Choisissez le format le plus compact qui permette aux utilisateurs de comprendre le résultat ou d’accomplir leur tâche.
Carte intégrée
Utilisez une carte intégrée pour présenter un résultat précis, une confirmation ou quelques actions. La carte doit se suffire à elle-même. Évitez d’y multiplier les niveaux de navigation.

Carrousel intégré
Utilisez un carrousel intégré pour permettre aux utilisateurs de parcourir rapidement quelques options similaires présentées en images et d’en choisir une.

Plein écran
Utilisez le plein écran pour les tâches qui demandent plus d’espace, par exemple pour consulter une carte, modifier du contenu sur un canevas ou parcourir des résultats en détail. Concevez l’interface pour qu’elle fonctionne avec la zone de saisie de ChatGPT, qui reste accessible en plein écran.

Incrustation d’image
Utilisez l’incrustation d’image pour qu’une activité en cours reste visible pendant la conversation, par exemple une session en direct, un jeu ou une vidéo.

Pour des recommandations détaillées sur la mise en page, les interactions, la conception visuelle et l’accessibilité, consultez les recommandations de conception d’interface.
Séparez le traitement des données du rendu de l’interface
Principe du découplage
Si vous joignez un gabarit de widget à chaque appel d’outil, ChatGPT risque de répéter trop souvent le rendu de votre iframe. Il est préférable de séparer les outils de traitement des données des outils de rendu :
- Les outils de traitement des données récupèrent, calculent ou modifient des données et renvoient uniquement les résultats de ces opérations.
- Les outils de rendu reçoivent les données finales et renvoient le gabarit du widget.
Le modèle peut ainsi analyser les données récupérées avant de décider d’afficher une interface. Il a alors bien plus de chances de répondre précisément à la demande de l’utilisateur.
Ce découplage fait partie de l’architecture MCP Apps.
En pratique, de nombreuses intégrations suivent cette répartition :
- Outils de recherche et de récupération (données d’abord) : ils renvoient des identifiants et des métadonnées sans y joindre de gabarit de widget.
- Outils de rendu (par exemple,
render_listings_widget) : ils reçoivent une liste d’identifiants déjà préparée et affichent le widget.
Seul l’outil de rendu doit inclure _meta.ui.resourceUri.
Séquence d’appels découplés
Séquence d’appels recommandée :
- Le modèle appelle l’outil de traitement des données (par exemple,
roll_dice). - Le modèle reçoit
structuredContentde cet outil. - Le modèle appelle l’outil de rendu avec ces données.
- Le widget s’affiche une seule fois avec le contexte final vérifié par le modèle.
Exemple de question complémentaire sur des annonces immobilières
Supposons que votre plugin affiche des annonces sous forme de cartes ainsi qu’un plan, mais que votre outil search côté serveur ne propose que des filtres généraux (ville, prix, nombre de chambres et de salles de bains), sans possibilité de filtrer par secteur scolaire.
Si un utilisateur demande « Parmi ces logements, lesquels se trouvent dans le secteur de la Richmond Primary School ? », le découplage permet de procéder ainsi :
searcheffectue une recherche large et renvoie les identifiants des annonces susceptibles de convenir, ainsi que leurs métadonnées.- Le modèle affine cette sélection pour répondre à la question complémentaire.
- Le modèle appelle
render_listings_widgetavec les seuls identifiants retenus. - Le widget affiche la sélection finale.
Bonnes pratiques :
- Gardez les outils de traitement des données réutilisables. Renvoyez un
structuredContentcomplet pour pouvoir enchaîner les appels. - Réservez les outils de rendu à la présentation. N’intégrez pas de logique métier dans le gestionnaire de rendu.
- Précisez la dépendance dans la description de l’outil de rendu (par exemple, « Appelez toujours
roll_diceen premier »). - Ne relancez un outil que si l’interaction le nécessite. Pour une action locale comme « Relancer le dé », laissez l’interface appeler directement l’outil de traitement des données sans remonter le widget.
Exemple de découplage
Exemple avec deux outils distincts pour lancer un dé et afficher le résultat :
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod/v3";
import { build } from "esbuild";
import { fileURLToPath } from "node:url";
const TEMPLATE_URI = "ui://widget/dice.html";
const server = new McpServer(
{ name: "Decoupled dice", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Bundle the same browser source that is type-checked and tested directly.
const widgetBundle = await build({
entryPoints: [fileURLToPath(new URL("./dice-widget.ts", import.meta.url))],
bundle: true,
format: "esm",
platform: "browser",
write: false,
});
const widgetScript = widgetBundle.outputFiles[0].text.replace(
/<\/script/gi,
"<\\/script"
);
const widgetHtml = `<!doctype html><html lang="en"><meta charset="utf-8"><title>Dice widget</title><body><script type="module">${widgetScript}</script></body></html>`;
server.registerResource("dice-widget", TEMPLATE_URI, {}, async () => ({
contents: [
{
uri: TEMPLATE_URI,
mimeType: "text/html;profile=mcp-app",
text: widgetHtml,
_meta: { ui: { prefersBorder: true } },
},
],
}));
// 1) Data tool: no output template, returns chainable structuredContent.
server.registerTool(
"roll_dice",
{
title: "Roll dice",
description: "Roll an N-sided die and return { sides, value }.",
inputSchema: { sides: z.number().int().min(2) },
outputSchema: {
sides: z.number().int().min(2),
value: z.number().int().min(1),
},
_meta: {
"openai/toolInvocation/invoking": "Rolling…",
"openai/toolInvocation/invoked": "Rolled.",
},
},
async ({ sides }) => {
const value = 1 + Math.floor(Math.random() * sides);
return {
structuredContent: { sides, value },
content: [{ type: "text", text: `Rolled ${value} on ${sides} sides.` }],
};
}
);
// 2) Render tool: owns the template and requires data from roll_dice.
server.registerTool(
"render_dice_widget",
{
title: "Render dice widget",
description:
"Render the dice widget from roll data. First call roll_dice, then pass its sides and value to this tool.",
inputSchema: {
sides: z.number().int().min(2),
value: z.number().int().min(1),
},
outputSchema: {
sides: z.number().int().min(2),
value: z.number().int().min(1),
},
_meta: {
ui: { resourceUri: TEMPLATE_URI },
"openai/toolInvocation/invoking": "Rendering…",
"openai/toolInvocation/invoked": "Rendered.",
},
},
async ({ sides, value }) => ({
structuredContent: { sides, value },
content: [
{
type: "text",
text: `Showing a ${sides}-sided roll: ${value}.`,
},
],
})
);
export default server;Installez esbuild avec les dépendances du serveur, puis enregistrez le code suivant, destiné au navigateur, sous le nom dice-widget.ts dans le même répertoire que le fichier du serveur :
import { App } from "@modelcontextprotocol/ext-apps";
const container = document.createElement("div");
container.style.cssText = "font-family: system-ui; padding: 8px;";
const resultLine = document.createElement("div");
resultLine.style.cssText = "font-size: 20px; margin-bottom: 6px;";
const output = document.createElement("span");
output.id = "out";
output.textContent = "—";
resultLine.append("Result: ", output);
const rerollButton = document.createElement("button");
rerollButton.id = "reroll";
rerollButton.textContent = "Re-roll";
rerollButton.disabled = true;
container.append(resultLine, rerollButton);
document.body.append(container);
const app = new App({ name: "Dice widget", version: "1.0.0" });
let latestInput: Record<string, unknown> = {};
let latestOutput: Record<string, unknown> = {};
function render(result: Record<string, unknown>) {
latestOutput = result;
output.textContent = String(result.value ?? "—");
}
app.ontoolinput = ({ arguments: input }) => {
latestInput = input ?? {};
};
app.ontoolresult = (result) => {
render(result.structuredContent ?? {});
};
// Call the data tool without remounting the widget.
rerollButton.onclick = async () => {
const candidate = latestOutput.sides ?? latestInput.sides;
const sides =
typeof candidate === "number" &&
Number.isInteger(candidate) &&
candidate >= 2
? candidate
: 6;
const next = await app.callServerTool({
name: "roll_dice",
arguments: { sides },
});
if (next.structuredContent) render(next.structuredContent);
};
await app.connect();
rerollButton.disabled = false;Gérez l’état
Une interface fournie par un serveur MCP utilise trois types d’état :
| Type d’état | Système responsable | Durée de vie | Exemples |
|---|---|---|---|
| Données métier (faisant autorité) | Serveur MCP ou service externe | Longue durée | Tâches, tickets, documents |
| État de l’interface (éphémère) | Instance de l’interface | Tant que l’instance est active | Ligne sélectionnée, panneau déplié, ordre de tri |
| État partagé entre les sessions (persistant) | Stockage que vous contrôlez | D’une session et d’une conversation à l’autre | Filtres enregistrés, mode d’affichage, espace de travail |
Conservez chaque valeur dans le système qui en est responsable. L’interface doit afficher les données de référence renvoyées par les outils et leur appliquer son état de présentation temporaire.
MCP server or external service
│
├── Authoritative business data
│
▼
UI
│
├── Ephemeral presentation state
│
└── Rendered view = business data + UI state
Conservez les données métier sur le serveur
Les données métier font autorité. Ne les stockez pas uniquement dans l’interface. Lorsqu’un utilisateur effectue une action :
- L’interface appelle un outil MCP.
- Le serveur valide la requête et met à jour les données.
- Le serveur renvoie un instantané à jour qui fait autorité.
- L’interface affiche cet instantané en conservant les éléments de son état de présentation qui restent compatibles.
Renvoyez suffisamment de contenu structuré pour que le modèle et l’interface puissent tous deux interpréter le nouvel état. La conversation reste ainsi utile même si l’interface ne se charge pas.
Conservez l’état temporaire de l’interface dans l’interface
Utilisez le mécanisme de gestion d’état de votre framework pour les valeurs qui concernent uniquement la présentation, comme l’élément sélectionné, le panneau ouvert ou un filtre en cours de définition. Chaque instance affichée de l’interface possède son propre état.
Si le modèle doit connaître la sélection en cours ou une modification en attente de validation, transmettez-lui cette information via ui/update-model-context. Ce mécanisme portable de MCP Apps permet de mettre à jour le contexte accessible au modèle.
ChatGPT permet également, à titre facultatif, de conserver l’état d’un widget :
- Lisez l’instantané actuel dans
window.openai.widgetState. - Enregistrez un nouvel instantané avec
window.openai.setWidgetState(state).
setWidgetState est synchrone. Appelez cette méthode après chaque modification significative de l’état de l’interface, sans utiliser await.
import { useState } from "react";
export function TaskList({
tasks,
}: {
tasks: { id: string; title: string }[];
}) {
const [state, setState] = useState(
window.openai?.widgetState ?? { selectedId: null }
);
function selectTask(selectedId: string) {
const nextState = { ...state, selectedId };
setState(nextState);
window.openai?.setWidgetState?.(nextState);
}
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<button
type="button"
aria-pressed={state.selectedId === task.id}
onClick={() => selectTask(task.id)}
>
{task.title}
</button>
</li>
))}
</ul>
);
}L’état du widget est propre à une seule instance affichée de l’interface. Ne l’utilisez ni comme source de référence pour les données métier ni comme stockage persistant.
Rendez les images accessibles au modèle
Si votre interface manipule des images, utilisez la structure suivante pour l’état du widget :
modelContent: texte ou données JSON à transmettre au modèle.privateContent: état réservé à l’interface, auquel le modèle ne doit pas avoir accès.imageIds: identifiants des fichiers que le modèle doit recevoir lors des prochains échanges.
const selectedFiles = await window.openai.selectFiles();
window.openai.setWidgetState({
modelContent: "Review the currently selected images.",
privateContent: {
currentView: "image-viewer",
filters: ["crop", "sharpen"],
},
imageIds: selectedFiles.map((file) => file.fileId),
});Incluez uniquement les identifiants de fichiers importés avec window.openai.uploadFile, sélectionnés avec window.openai.selectFiles, reçus via les paramètres de fichiers en entrée d’un outil ou renvoyés sous forme de références de fichiers dans ses résultats.
Stockez sur votre serveur l’état partagé entre les sessions
Conservez dans un stockage que vous contrôlez les préférences et les données qui doivent rester disponibles d’une conversation, d’un appareil ou d’une session à l’autre. Authentifiez l’utilisateur pour que le serveur MCP puisse associer chaque requête au bon compte.
Lorsque vous ajoutez un stockage persistant :
- Maintenez une latence suffisamment faible pour que l’interface reste réactive.
- Protégez les données privées en vérifiant les autorisations côté serveur.
- Tenez compte des exigences de résidence des données et de conformité.
- Appliquez des limites de débit aux requêtes provenant de nouvelles tentatives ou d’instances de l’interface actives en parallèle.
- Attribuez une version aux objets stockés pour pouvoir les migrer sans perturber les conversations existantes.
Évitez d’utiliser localStorage pour les données d’état essentielles. L’interface s’exécute dans une iframe isolée, et le stockage du navigateur ne permet pas de partager ces données de manière fiable entre appareils ou entre sessions.
Préparez la structure du projet de composant
Une fois que vous connaissez la passerelle MCP Apps et les extensions ChatGPT facultatives, vous pouvez créer la structure de votre projet de composant.
Il est recommandé de séparer le code du composant de la logique du serveur. Voici une organisation courante :
plugin-ui/
server/ # MCP server (Python or Node)
web/ # Component bundle source
package.json
tsconfig.json
src/component.tsx
dist/component.js # Build output
Créez le projet et installez les dépendances (Node 18 ou version ultérieure recommandé) :
cd plugin-ui/web
npm init -y
npm install react@^19 react-dom@^19
npm install -D typescript esbuild
Si votre composant nécessite des bibliothèques pour le glisser-déposer, les graphiques ou d’autres fonctionnalités, ajoutez-les maintenant. Limitez les dépendances pour réduire la taille du bundle.
Écrivez le composant React
Le fichier qui sert de point d’entrée doit monter un composant dans un élément root et afficher le dernier résultat d’outil transmis par la passerelle MCP Apps (par exemple, via ui/notifications/tool-result).
La page d’exemples présente plusieurs interfaces, dont la liste de pizzerias de Pizzaz.
Explorez la galerie de composants Pizzaz
Les exemples d’interfaces comprennent plusieurs composants qui peuvent vous servir de modèles pour concevoir votre propre interface :
- Liste Pizzaz : liste ordonnée de cartes avec gestion des favoris et boutons d’action.

- Carrousel Pizzaz : carrousel horizontal basé sur Embla qui illustre une mise en page riche en contenus multimédias.

- Carte Pizzaz : intégration de Mapbox avec un inspecteur en plein écran et synchronisation de l’état avec l’hôte.

- Album Pizzaz : galerie en piles d’images pour découvrir un lieu en détail.

- Vidéo Pizzaz : lecteur piloté par script avec éléments en surimpression et commandes de plein écran.
Chaque exemple montre comment regrouper les ressources, connecter les API de l’hôte et structurer l’état pour une conversation réelle. Copiez l’exemple le plus proche de votre cas d’usage et adaptez la couche de données aux réponses de vos outils.
Hooks utilitaires React
Voici un hook qui permet de s’abonner à ui/notifications/tool-result :
import { useEffect, useState } from "react";
type ToolResult = { structuredContent?: unknown } | null;
export function useToolResult() {
const [toolResult, setToolResult] = useState<ToolResult>(null);
useEffect(() => {
const onMessage = (event: MessageEvent) => {
if (event.source !== window.parent) return;
const message = event.data;
if (!message || message.jsonrpc !== "2.0") return;
if (message.method !== "ui/notifications/tool-result") return;
setToolResult(message.params ?? null);
};
window.addEventListener("message", onMessage, { passive: true });
return () => window.removeEventListener("message", onMessage);
}, []);
return toolResult;
}Utilisez toolResult?.structuredContent pour le rendu et traitez son contenu comme des données d’entrée non fiables.
Localisation du widget
L’hôte transmet ses paramètres régionaux via document.documentElement.lang. Utilisez-les pour charger les traductions et mettre en forme les dates et les nombres. Exécutez npm install react-intl dans le répertoire web/ avant d’utiliser cet exemple :
import { IntlProvider, FormattedMessage } from "react-intl";
const messages: Record<string, Record<string, string>> = {
"en-US": { greeting: "Hello" },
"es-ES": { greeting: "Hola" },
};
export function PluginUI() {
const locale = document.documentElement.lang || "en-US";
return (
<IntlProvider
locale={locale}
messages={messages[locale] ?? messages["en-US"]}
>
<FormattedMessage id="greeting" defaultMessage="Hello" />
</IntlProvider>
);
}Créez le bundle pour l’iframe
Une fois votre composant React écrit, vous pouvez le compiler en un seul module JavaScript que le serveur pourra intégrer directement au HTML :
// package.json
{
"scripts": {
"build": "esbuild src/component.tsx --bundle --format=esm --jsx=automatic --outfile=dist/component.js"
}
}
Exécutez npm run build pour générer dist/component.js. Si esbuild signale des dépendances manquantes, vérifiez que vous avez exécuté npm install dans le répertoire web/ et que les noms utilisés dans vos imports correspondent à ceux des paquets installés (par exemple, vérifiez s’il s’agit de @react-dnd/html5-server-side ou de react-dnd-html5-server-side).
Intégrez le composant à la réponse du serveur
Exposez le composant comme une ressource MCP avec le type MIME des interfaces MCP Apps (text/html;profile=mcp-app). Si vous utilisez @modelcontextprotocol/ext-apps/server, privilégiez RESOURCE_MIME_TYPE plutôt que d’inscrire la chaîne en dur :
import {
registerAppResource,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { readFileSync } from "node:fs";
const component = readFileSync("web/dist/component.js", "utf8");
registerAppResource(
server,
"project-board",
"ui://project-board/v1.html",
{},
async () => ({
contents: [
{
uri: "ui://project-board/v1.html",
mimeType: RESOURCE_MIME_TYPE,
text: `<div id="root"></div><script type="module">${component}</script>`,
_meta: {
ui: {
prefersBorder: true,
domain: "https://example.com",
csp: {
connectDomains: ["https://api.example.com"],
resourceDomains: ["https://static.example.com"],
},
},
},
},
],
})
);Associez l’URI de la ressource uniquement aux outils qui doivent afficher le composant. Pour assurer la compatibilité avec davantage d’hôtes MCP Apps, utilisez _meta.ui.resourceUri. ChatGPT reconnaît également _meta["openai/outputTemplate"] comme alias de compatibilité.
Traitez l’URI de la ressource comme une clé de cache. Si vous apportez au HTML, au JavaScript ou au CSS une modification qui rompt la compatibilité, publiez une nouvelle URI et mettez à jour chaque outil qui y fait référence.
Politique de sécurité du contenu (CSP)
Déclarez précisément les domaines auxquels le composant se connecte ou depuis lesquels il charge des ressources :
connectDomainspour les requêtes API.resourceDomainspour les scripts, les styles, les images et les autres ressources.frameDomainsuniquement lorsque le composant doit intégrer des iframes provenant d’origines précises.
Les cadres imbriqués sont bloqués par défaut. Limitez chaque liste d’autorisation au strict nécessaire. Lors de l’examen du plugin, la politique déclarée est comparée au comportement de l’interface.
Vous pouvez intégrer un éditeur ou une interface d’administration existante qui relève du même domaine enregistrable que votre serveur MCP. Par exemple, un serveur à l’adresse https://api.example.com/mcp peut déclarer https://app.example.com dans frameDomains. Lors de la soumission du plugin, fournissez la justification requise et respectez la politique relative aux iframes, notamment les restrictions sur l’hébergement mutualisé et les exigences d’examen.
En production, privilégiez les gabarits d’interface des composants.
Pendant le développement, vous pouvez reconstruire le bundle du composant à chaque modification de votre code React et recharger le serveur à chaud.
Proposez le paiement dans votre interface
Pour permettre aux utilisateurs de finaliser leurs achats depuis l’interface de votre plugin, utilisez le composant pour présenter les produits, les prix, les conditions et les modes de paiement avant la confirmation. Veillez à ce que les outils de gestion du catalogue et des commandes restent utiles sans interface. Choisissez ensuite un parcours de paiement externe ou, si cette possibilité vous est offerte, une option de paiement intégrée.
Utilisez le paiement externe par défaut
Le paiement externe est la solution recommandée et accessible de manière générale. Depuis le composant, ajoutez un lien vers un parcours de paiement que vous hébergez, en tant que marchand, sur votre propre domaine. Vous y gérez :
- Les prix et l’encaissement.
- Les taxes, les remises et les frais.
- L’expédition et le traitement des commandes.
- Les remboursements, l’assistance et la conformité.
Pour le moment, seuls les plugins destinés à l’achat de biens physiques peuvent être approuvés. Ne proposez pas d’autres catégories de transactions, sauf si OpenAI les a explicitement activées pour votre plugin.
Utilisez les moyens de paiement enregistrés
Pour les achats de biens physiques éligibles, une interface facultative peut permettre aux clients de sélectionner un moyen de paiement déjà enregistré auprès de votre service. Ce parcours peut afficher les moyens de paiement enregistrés éligibles, mais ne peut pas recueillir de nouvelles coordonnées de paiement. Votre serveur MCP traite l’achat et renvoie le résultat de la commande qui fait autorité.
Utilisez le panneau de paiement de ChatGPT
Le paiement intégré via le panneau de paiement de ChatGPT est en bêta privée pour certaines places de marché. Seuls certains développeurs et utilisateurs y ont accès.
Pour les intégrations qui en bénéficient, window.openai.requestCheckout ouvre le panneau de paiement de ChatGPT :
const order = await window.openai.requestCheckout(checkoutSession);Le parcours de paiement comporte quatre étapes :
- Un outil MCP renvoie une session de paiement dans
structuredContent. - Le composant affiche les lignes de commande, les totaux, les conditions et les options de traitement de la commande.
- Le composant appelle
requestCheckout(checkoutSession)une fois que l’utilisateur a choisi de payer. - ChatGPT envoie le jeton de paiement sélectionné à l’outil
complete_checkoutdu serveur MCP. Cet outil débite le moyen de paiement et renvoie la commande finalisée.
La session de paiement doit inclure :
- Un identifiant de session unique.
- Les lignes de commande et les quantités.
- Les totaux exprimés en nombres entiers dans la plus petite unité de la devise.
- Les métadonnées du prestataire de paiement et du marchand.
- Les liens requis pour les informations légales, la confidentialité, les remboursements et l’assistance.
Le serveur doit faire autorité pour les prix et le statut des commandes. Vérifiez le jeton de paiement, rendez l’opération idempotente, enregistrez durablement la commande et renvoyez un reçu qui fait foi. Ne vous fiez jamais à des totaux calculés uniquement dans le composant.
Utilisez payment_mode: "test" pour tester le parcours de bout en bout sans transférer de fonds réels. Gérez les annulations, les refus de paiement et les erreurs du prestataire de paiement dans le composant.
Pour connaître tous les champs de la session de paiement, le comportement du prestataire, la structure du résultat de complete_checkout et les exigences relatives aux paiements délégués, consultez la référence de l’API de paiement.