Vue d’ensemble
Une interface utilisateur personnalisée est facultative. Ajoutez-en une lorsque le cas d’usage d’un plugin nécessite d’examiner, de comparer, de modifier, de confirmer ou de parcourir des informations structurées. Veillez à ce que les outils MCP restent utiles sans composant, afin que ChatGPT et Codex puissent mener le workflow à son terme sans interface utilisateur.
Le serveur MCP renvoie des ressources d’interface utilisateur pour certains outils. Les composants s’exécutent dans
une iframe au sein de ChatGPT, communiquent avec l’hôte via le pont MCP Apps
(JSON-RPC sur postMessage) et s’affichent à côté 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 utilisateur renvoyées par un serveur MCP. MCP Apps définit comment votre serveur associe les outils aux ressources d’interface utilisateur et comment l’iframe communique avec son hôte.
Pour une nouvelle interface utilisateur :
- Déclarez la ressource d’interface utilisateur avec
_meta.ui.resourceUri. - Utilisez le pont JSON-RPC
ui/*surpostMessagepour l’initialisation, les notifications, les appels d’outils, les messages et le contexte visible par le modèle. - Veillez à ce que les outils restent utiles sans interface utilisateur, afin que le modèle puisse mener le workflow à son terme dans les clients qui n’affichent pas les composants.
Cette base qui privilégie les standards permet à une même interface de fonctionner dans ChatGPT et sur d’autres hôtes compatibles avec MCP Apps.
Lorsque vous êtes prêt à implémenter le standard, appuyez-vous sur la spécification MCP Apps.
Ajoutez les extensions ChatGPT
Une fois le fonctionnement de MCP Apps en place, utilisez window.openai uniquement pour les fonctionnalités que
la spécification commune ne couvre pas. Ces extensions facultatives peuvent améliorer
l’expérience dans ChatGPT sans faire partie du socle portable
de l’interface utilisateur.
Privilégiez les champs et méthodes communs
Utilisez le champ ou la méthode MCP Apps dès que la spécification commune couvre la fonctionnalité :
| Objectif | Standard MCP Apps | Alias de compatibilité ChatGPT |
|---|---|---|
| Associer un outil à une ressource d’interface utilisateur | _meta.ui.resourceUri | _meta["openai/outputTemplate"] |
| Recevoir les données d’entrée de l’outil | ui/initialize + ui/notifications/tool-input | window.openai.toolInput |
| Recevoir les résultats de l’outil | ui/notifications/tool-result | window.openai.toolOutput |
| Appeler un outil depuis l’interface utilisateur | 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. Les nouvelles interfaces devraient utiliser les champs communs et les méthodes du pont figurant dans la colonne centrale.
Voici quelques exemples :
- Paiement instantané avec
window.openai.requestCheckout. - Gestion des fichiers ChatGPT avec
window.openai.uploadFile,window.openai.selectFilesetwindow.openai.getFileDownloadUrl. - Fenêtres modales contrôlées par l’hôte avec
window.openai.requestModal. - Persistance de l’état du widget avec
window.openai.widgetStateetwindow.openai.setWidgetState.
Détectez 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({
/* ... */
});
} else {
// Fallback behavior for hosts without this extension.
}
Évitez de conditionner le comportement au nom de l’hôte ou du produit. Vérifiez plutôt la disponibilité de la fonctionnalité dont votre interface a besoin.
Pour connaître les signatures des extensions et consulter des exemples, reportez-vous à la référence du pont
window.openai pour les composants.
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 utilisateur sur GitHub.
Choisissez un mode d’affichage
Commencez par une interface intégrée à la conversation et ne demandez plus d’espace que si le workflow l’exige. Choisissez le mode d’affichage le plus compact qui permette de comprendre le résultat ou d’accomplir la tâche.
Carte intégrée
Utilisez une carte intégrée pour un résultat ciblé, une confirmation ou un petit ensemble d’actions. Veillez à ce qu’elle se suffise à elle-même et évitez la navigation à plusieurs niveaux.

Carrousel intégré
Utilisez un carrousel intégré lorsque les utilisateurs doivent parcourir un petit ensemble d’options similaires, riches en éléments visuels, et faire un choix.

plein écran
Utilisez le plein écran pour les tâches riches en contenu qui nécessitent plus d’espace, comme les cartes, les canevas d’édition ou la navigation détaillée. Concevez l’expérience pour qu’elle fonctionne avec la zone de saisie de ChatGPT, qui reste disponible en plein écran.

Image dans l’image
Utilisez le mode image dans l’image pour une activité en cours qui doit rester visible pendant que la conversation se poursuit, comme une session en direct, un jeu ou une vidéo.

Pour des recommandations détaillées sur la mise en page, les interactions, le design visuel et l’accessibilité, consultez les recommandations pour les interfaces utilisateur.
Séparez le traitement des données du rendu de l’interface utilisateur
Architecture découplée
Si vous joignez un modèle de widget à chaque appel d’outil, ChatGPT risque de refaire trop souvent le rendu de votre iframe. Une meilleure approche consiste à séparer les outils de traitement des données des outils de rendu :
- Les outils de données récupèrent, calculent ou modifient des données et renvoient uniquement des résultats d’outils.
- Les outils de rendu reçoivent les données finales et renvoient le modèle de widget.
Le modèle peut ainsi appliquer son intelligence aux données récupérées avant de décider d’afficher une interface à l’utilisateur, ce qui augmente considérablement ses chances d’atteindre l’objectif précis exprimé par celui-ci.
Cette approche fait partie de l’architecture MCP Apps.
En pratique, de nombreuses intégrations d’interfaces utilisateur reposent sur cette séparation :
- Outils de recherche et de récupération (centrés sur les données) : renvoient des identifiants et des métadonnées, sans modèle de widget associé.
- Outils de rendu (par exemple,
render_listings_widget) : reçoivent une liste préparée d’identifiants et effectuent le rendu du widget.
Seul l’outil de rendu devrait inclure _meta.ui.resourceUri.
Séquence d’appels découplée
Séquence d’appels recommandée :
- Le modèle appelle l’outil de données (par exemple,
roll_dice). - Le modèle reçoit
structuredContentde l’outil de données. - 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 : questions complémentaires sur des annonces immobilières
Supposons que votre plugin affiche des fiches d’annonces et une carte, mais que votre outil search côté serveur
ne prenne en charge que des filtres généraux (ville, prix, nombre de chambres et de salles de bains) et ne permette pas de filtrer par
secteur scolaire.
Si un utilisateur demande : « Parmi ces biens, lesquels se trouvent dans le secteur scolaire de Richmond Primary School ? », le découplage est utile :
searcheffectue une recherche large et renvoie les identifiants des annonces candidates ainsi que leurs métadonnées.- Le modèle affine cette sélection en fonction de la question complémentaire.
- Le modèle appelle
render_listings_widgetavec uniquement les identifiants retenus après filtrage. - Le widget affiche la sélection finale filtrée.
Bonnes pratiques :
- Veillez à ce que les outils de données restent réutilisables. Renvoyez un
structuredContentcomplet pour permettre l’enchaînement des appels. - Limitez les outils de rendu à la présentation. N’intégrez pas de logique métier dans le gestionnaire de rendu.
- Indiquez la dépendance dans la description de l’outil de rendu (par exemple, « Appelez toujours
roll_diceen premier »). - Veillez à ce que les nouvelles exécutions soient intentionnelles. Laissez l’interface appeler directement les outils de données pour les interactions locales comme « Relancer les dés », sans remonter le widget.
Exemple de découplage
Exemple (outils de dés découplés) :
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod/v3";
const TEMPLATE_URI = "ui://widget/dice.html";
const server = new McpServer(
{ name: "Decoupled dice", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// The widget only renders the latest tool result.
// Re-roll calls the data tool directly to avoid remounting the widget.
const widgetHtml = `
<div style="font-family: system-ui; padding: 8px;">
<div style="font-size: 20px; margin-bottom: 6px;">
Result: <span id="out">—</span>
</div>
<button id="reroll">Re-roll</button>
</div>
<script>
const outputEl = document.getElementById("out");
const rerollButton = document.getElementById("reroll");
const pendingRequests = new Map();
let nextRequestId = 1;
let latestToolInput;
let latestToolOutput;
function render(result) {
outputEl.textContent = String(result?.value ?? "—");
}
function request(method, params) {
const id = nextRequestId++;
window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*");
return new Promise((resolve, reject) => {
pendingRequests.set(id, { resolve, reject });
});
}
window.addEventListener(
"message",
(event) => {
if (event.source !== window.parent) return;
const message = event.data;
if (!message || message.jsonrpc !== "2.0") return;
if (message.id !== undefined && pendingRequests.has(message.id)) {
const pending = pendingRequests.get(message.id);
pendingRequests.delete(message.id);
if (message.error) pending.reject(message.error);
else pending.resolve(message.result);
return;
}
if (message.method === "ui/notifications/tool-input") {
latestToolInput = message.params;
}
if (message.method === "ui/notifications/tool-result") {
latestToolOutput = message.params?.structuredContent;
render(latestToolOutput);
}
},
{ passive: true }
);
rerollButton.onclick = async () => {
const sides = latestToolOutput?.sides ?? latestToolInput?.sides ?? 6;
const next = await request("tools/call", {
name: "roll_dice",
arguments: { sides },
});
if (next?.structuredContent) {
render(next.structuredContent);
}
};
</script>
`.trim();
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;
Gérez l’état
Une interface fournie par un serveur MCP utilise trois types d’état :
| Type d’état | 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 de l’interface est active | Ligne sélectionnée, panneau déplié, ordre de tri |
| État conservé entre les sessions (durable) | 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 faisant autorité issues des résultats des outils et y superposer un é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 constituent la source de vérité. 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 l’instantané de référence mis à jour.
- L’interface affiche l’instantané tout en préservant les éléments compatibles de l’état de présentation.
Renvoyez suffisamment de contenu structuré pour que le modèle et l’interface puissent comprendre le nouvel état. La conversation reste ainsi utile même si l’interface ne peut pas se charger.
Conservez l’état temporaire de l’interface dans l’interface
Utilisez la gestion d’état du framework pour les valeurs qui concernent uniquement la présentation, comme un élément sélectionné, un panneau ouvert ou un filtre en cours de définition. Chaque instance affichée de l’interface possède son propre état.
Lorsque le modèle doit être informé d’une sélection ou d’une modification en attente, transmettez ces
informations via ui/update-model-context. Il s’agit du mécanisme portable de MCP Apps
permettant de mettre à jour le contexte visible par le modèle.
ChatGPT propose également une persistance facultative limitée au widget :
- Lisez l’instantané actuel dans
window.openai.widgetState. - Écrivez un nouvel instantané avec
window.openai.setWidgetState(state).
setWidgetState est synchrone. Appelez cette fonction après chaque modification significative de l’état de l’interface ;
il n’y a rien à attendre avec await.
import { useState } from "react";
export function TaskList({ tasks }) {
const [state, setState] = useState(
window.openai?.widgetState ?? { selectedId: null }
);
function selectTask(selectedId) {
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 appartient à une seule instance affichée de l’interface. Ne l’utilisez pas comme source de vérité pour les données métier ni comme stockage durable.
Rendez les images visibles par le modèle
Pour une interface qui utilise des images, adoptez ce format structuré pour l’état du widget :
modelContent: texte ou JSON que le modèle doit voir.privateContent: état réservé à l’interface, que le modèle ne doit pas voir.imageIds: identifiants de fichiers que le modèle doit recevoir lors des échanges suivants.
window.openai.setWidgetState({
modelContent: "Review the currently selected images.",
privateContent: {
currentView: "image-viewer",
filters: ["crop", "sharpen"],
},
imageIds: ["file_123", "file_456"],
});
N’incluez que 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 des outils ou
renvoyés via les références de fichiers dans les résultats des outils.
Stockez sur votre serveur l’état à conserver entre les sessions
Stockez les préférences et les données qui doivent être conservées d’une conversation, d’un appareil ou d’une session à l’autre dans un stockage que vous contrôlez. Authentifiez l’utilisateur pour que le serveur MCP puisse associer chaque requête au bon compte.
Lorsque vous ajoutez un stockage durable :
- Maintenez une latence suffisamment faible pour que l’interface reste interactive.
- Protégez les données privées grâce à des contrôles d’autorisation côté serveur.
- Anticipez les exigences de résidence des données et de conformité.
- Appliquez des limites de débit au trafic provenant des nouvelles tentatives ou des instances d’interface simultanées.
- Versionnez les objets stockés pour pouvoir les migrer sans perturber les conversations existantes.
Évitez localStorage pour les données d’état essentielles. L’interface s’exécute dans une iframe isolée, et le stockage du navigateur
ne fournit pas de couche de données fiable entre appareils ou entre sessions.
Créez la structure du projet de composant
Maintenant que vous comprenez le fonctionnement du pont MCP Apps et des extensions facultatives de ChatGPT, vous pouvez créer la structure de votre projet de composant.
Il est recommandé de séparer le code du composant de la logique 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@^18 react-dom@^18
npm install -D typescript esbuild
Si votre composant nécessite des bibliothèques de glisser-déposer, de graphiques ou d’autres fonctionnalités, ajoutez-les maintenant. Limitez les dépendances pour réduire la taille du bundle.
Écrivez le composant React
Votre fichier d’entrée doit monter un composant dans un élément root et effectuer le rendu à partir
du dernier résultat d’outil transmis par le pont MCP Apps (par exemple,
ui/notifications/tool-result).
La page d’exemples présente des exemples d’interfaces, comme la liste Pizzaz de pizzerias.
Explorez la galerie de composants Pizzaz
Les exemples d’interfaces comprennent des exemples de composants. Utilisez-les comme modèles pour concevoir votre propre interface :
- Pizzaz List : liste de cartes classées avec favoris et boutons d’appel à l’action.

- Pizzaz Carousel : carrousel horizontal basé sur Embla, illustrant des mises en page riches en médias.

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

- Pizzaz Album : galerie d’éléments empilés, conçue pour explorer un lieu en détail.

- Pizzaz Video : lecteur piloté par script avec éléments superposés et commandes de plein écran.
Chaque exemple montre comment regrouper les ressources dans un bundle, connecter les API de l’hôte et structurer l’état pour des conversations réelles. Copiez celui qui se rapproche le plus de votre cas d’usage et adaptez la couche de données aux réponses de vos outils.
Hooks utilitaires React
Un petit utilitaire pour s’abonner à ui/notifications/tool-result :
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;
}
Effectuez le rendu à partir de toolResult?.structuredContent et traitez ce contenu comme une entrée non fiable.
Localisation du widget
L’hôte reporte les paramètres régionaux dans document.documentElement.lang. Utilisez ces paramètres
pour charger les traductions et formater les dates et les nombres. Voici une approche courante avec
react-intl :
import { IntlProvider } from "react-intl";
import en from "./locales/en-US.json";
import es from "./locales/es-ES.json";
const messages: Record<string, Record<string, string>> = {
"en-US": en,
"es-ES": es,
};
export function PluginUI() {
const locale = document.documentElement.lang || "en-US";
return (
<IntlProvider
locale={locale}
messages={messages[locale] ?? messages["en-US"]}
>
{/* Render UI with <FormattedMessage> or useIntl() */}
</IntlProvider>
);
}
Créez le bundle pour l’iframe
Une fois votre composant React écrit, vous pouvez le compiler en un module JavaScript unique que le serveur peut intégrer directement :
// package.json
{
"scripts": {
"build": "esbuild src/component.tsx --bundle --format=esm --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 vos imports correspondent aux noms des paquets installés (par exemple, @react-dnd/html5-server-side ou react-dnd-html5-server-side).
Intégrez le composant à la réponse du serveur
Exposez le composant en tant que ressource MCP avec le type MIME d’interface de MCP Apps
(text/html;profile=mcp-app). Si vous utilisez
@modelcontextprotocol/ext-apps/server, privilégiez RESOURCE_MIME_TYPE plutôt que
d’insérer 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 une compatibilité plus large avec 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. Lorsque vous apportez une modification incompatible au HTML, au JavaScript ou au CSS, 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 spécifiques.
Les cadres imbriqués sont bloqués par défaut. Limitez chaque liste d’autorisation au strict nécessaire. Le processus de révision des plugins vérifie que le comportement de l’interface respecte la politique déclarée.
Vous pouvez intégrer un éditeur ou une interface d’administration existants depuis le domaine
enregistrable propre à votre serveur MCP. Par exemple, un serveur à l’adresse https://api.example.com/mcp peut
déclarer https://app.example.com dans frameDomains. Fournissez la justification requise
lors de la soumission et respectez la
politique relative aux iframes, notamment
ses restrictions sur l’hébergement mutualisé et ses exigences de révision.
Les modèles d’interface de composants constituent l’approche recommandée pour la production.
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
Si vous souhaitez permettre aux utilisateurs de payer dans les parcours de l’interface de votre plugin, utilisez le composant pour présenter les produits, les prix, les conditions et les options de paiement avant la confirmation. Veillez à ce que les outils sous-jacents de catalogue et de commande restent utiles sans interface, puis choisissez un parcours de paiement externe ou, si elle est disponible, une option de paiement intégrée.
Utilisez le paiement externe par défaut
Le paiement externe est l’approche recommandée et accessible à tous. Depuis le composant, ajoutez un lien vers un parcours de paiement hébergé par le marchand sur votre propre domaine, où vous gérez :
- Les tarifs et l’encaissement des paiements.
- Les taxes, les remises et les frais.
- L’expédition et l’exécution des commandes.
- Les remboursements, l’assistance et la conformité.
L’approbation actuelle se limite aux plugins permettant l’achat de biens physiques. Ne proposez pas d’autres catégories commerciales, sauf si OpenAI les a explicitement activées pour votre plugin.
Utilisez des 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 précédemment enregistré auprès de votre service. Ce parcours peut afficher les moyens de paiement enregistrés éligibles, mais ne peut pas recueillir de nouvelles informations de paiement. Votre serveur MCP traite l’achat et renvoie le résultat de la commande qui fait autorité.
Utilisez la fenêtre de paiement ChatGPT
Le paiement intégré avec la fenêtre de paiement ChatGPT est en bêta privée pour certaines marketplaces et n’est pas accessible à l’ensemble des développeurs ou des utilisateurs.
Pour les intégrations où cette fonctionnalité est activée, window.openai.requestCheckout ouvre la fenêtre
de paiement 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 d’exécution de la commande.
- Le composant appelle
requestCheckout(checkoutSession)après que l’utilisateur a choisi de payer. - ChatGPT envoie le token de paiement sélectionné à l’outil
complete_checkoutdu serveur MCP, qui 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 d’unités monétaires mineures.
- Métadonnées du prestataire de paiement et du marchand.
- Liens obligatoires vers les informations juridiques, de confidentialité, de remboursement et d’assistance.
Considérez le serveur comme la source de vérité pour les prix et l’état des commandes. Vérifiez le token de paiement, rendez l’opération idempotente, enregistrez la commande de façon persistante et renvoyez un reçu faisant autorité. Ne vous fiez jamais aux 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 paiements refusés 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 de paiement,
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.