How to Integrate External Services with an OpenAI Plugin: A Complete Guide
To integrate external services with an OpenAI plugin, you create a declarative manifest, implement explicit-only skills that handle HTTP requests securely via environment variables, and optionally chain them into deterministic commands.
The openai/plugins repository (also known as the Codex plugins monorepo) provides a standardized framework for exposing REST APIs, SDKs, and OAuth-protected resources to Large Language Models (LLMs). This architecture ensures that every external call is predictable, auditable, and secure by design.
Architecture of External Service Integration
OpenAI plugins use a layered architecture that separates user interaction from external API execution. When you integrate external services with an OpenAI plugin, the request flows through explicit skill invocations rather than implicit tool calls.
The architectural flow follows this pattern:
- User Prompt triggers a
/commandor direct$skillreference - Command layer (optional) orchestrates multi-step workflows
- Skill layer performs the actual external service call via Python or Node.js implementations
- Implementation scripts load credentials from
.envfiles, build HTTP requests, and return JSON payloads - LLM receives the structured result to continue the conversation
In plugins/zoom/skills/rest-api/SKILL.md, this pattern is demonstrated through a REST API skill that explicitly declares its capabilities and refuses implicit invocation via policy.allow_implicit_invocation: false.
Core Components for Service Integration
Plugin Manifest (plugin.json)
Every plugin requires a machine-readable manifest located at .codex-plugin/plugin.json. This file describes the plugin's capabilities, entry points, and metadata. The Zoom plugin manifest at plugins/zoom/.codex-plugin/plugin.json serves as the canonical reference, defining skills, commands, and assets that Codex can discover.
Explicit-Only Skills (SKILL.md)
Skills are the primary mechanism to integrate external services with an OpenAI plugin. Each skill contains:
- Declarative metadata in
SKILL.mdspecifying input/output schemas and invocation policies - Implementation code in
scripts/directories (commonly Python) that execute HTTP requests - Security boundaries ensuring credentials never reach the LLM context window
The plugins/zoom/skills/rest-api/SKILL.md file illustrates how to document endpoint contracts while keeping the actual request logic in separate implementation files.
Deterministic Commands (commands/)
Commands chain multiple skills into reusable workflows. The file plugins/zoom/commands/build-zoom-rest-api-app.md demonstrates how to sequence authentication, API calls, and verification steps into a single deterministic path that users invoke via /build-zoom-rest-api-app.
Reviewer Agents (agents/)
Security-focused agents audit integrations before deployment. The plugins/zoom/agents/zoom-integration-reviewer.md file shows how automated reviewers validate that external service calls follow security protocols and handle errors correctly.
Step-by-Step Guide to Integrate External Services
Follow these eight steps to add a new external service integration to the openai/plugins repository:
-
Create the plugin directory structure
mkdir -p plugins/my-service/skills/my-service/scripts mkdir -p plugins/my-service/commands -
Define the plugin manifest
Copy the structure from
plugins/zoom/.codex-plugin/plugin.jsonand adjust the name, description, and capabilities fields to match your external service. -
Write the skill manifest
Create
skills/my-service/SKILL.mdwith explicit invocation policies:policy: allow_implicit_invocation: falseInclude input/output schemas and example prompt snippets.
-
Implement the skill handler
Create
skills/my-service/scripts/call_api.py:import os import requests def handler(event: dict) -> dict: """ Expected event shape: { "url": "https://api.example.com/v1/resource", "params": {"q": "search term"} } """ api_url = event["url"] params = event.get("params", {}) token = os.getenv("MY_SERVICE_TOKEN") # Never commit to repo headers = {"Authorization": f"Bearer {token}"} try: resp = requests.get( api_url, headers=headers, params=params, timeout=10 ) resp.raise_for_status() data = resp.json() except requests.RequestException as e: return {"error": str(e), "status": "failed"} return {"status": "ok", "data": data} -
Configure the runner
Update
SKILL.mdto point to your script via theagents/openai.yamlrunner configuration, mapping skill invocations to the handler function. -
Add optional commands
For multi-step workflows, create a command file in
commands/following the format ofplugins/zoom/commands/build-zoom-rest-api-app.mdto chain authentication and API calls. -
Write tests
Verify request formation, error handling, and output shapes by adding tests in
skills/my-service/tests/, mirroring the structure inplugins/zoom/skills/rest-api/tests/. -
Commit and sync
Push to the repository; Codex discovers the plugin automatically once the repository syncs.
Security Best Practices
When you integrate external services with an OpenAI plugin, credential isolation is mandatory. The reference implementation in plugins/zoom/skills/rest-api/examples/webhook-server.md demonstrates loading secrets from .env files rather than hardcoding them or exposing them to the LLM.
Key security requirements:
- Explicit invocation only: Set
policy.allow_implicit_invocation: falsein everySKILL.mdto prevent unintended API calls - Environment variable isolation: Store
API_TOKEN,CLIENT_SECRET, and OAuth credentials in.envfiles excluded from version control - Timeout configuration: Implement 10-second timeouts on all HTTP requests to prevent hanging connections
- Error sanitization: Return structured error objects rather than raw stack traces to the LLM
Summary
- Plugin manifests (
plugin.json) declare capabilities and entry points for Codex discovery - Explicit-only skills handle external HTTP requests through isolated Python/Node scripts that read credentials from
.envfiles - Commands provide deterministic workflows that chain multiple skills for complex integrations
- Reviewer agents audit code for security vulnerabilities before deployment
- The Zoom plugin in
plugins/zoom/provides a battle-tested blueprint for REST API integrations, webhook handling, and OAuth flows
Frequently Asked Questions
How do I prevent the LLM from calling external APIs accidentally?
Set policy.allow_implicit_invocation: false in your SKILL.md manifest. This forces users to explicitly reference the skill (e.g., $rest-api) rather than allowing the model to invoke it automatically during conversation. According to the openai/plugins source code, this explicit-only policy ensures every external service call is intentional and auditable.
Where should I store API keys and OAuth tokens?
Store sensitive credentials in a .env file at the plugin root, never in the repository or skill manifests. The implementation scripts in plugins/zoom/skills/rest-api/examples/ load tokens via os.getenv() at runtime, keeping secrets isolated from the LLM context and version control.
Can I use languages other than Python for skill implementations?
While Python is the primary language shown in the repository (see call_api.py patterns in the Zoom plugin), the framework supports any executable via the runner configuration in agents/openai.yaml. Node.js, Go, or shell scripts are valid alternatives provided they accept JSON input via stdin or event parameters and return JSON-serializable output.
How do I handle rate limiting from external APIs?
Implement retry logic with exponential backoff in your skill's handler function, similar to the error handling patterns in plugins/zoom/skills/rest-api/. Return structured error objects like {"error": "Rate limit exceeded", "retry_after": 60} so commands can implement fallback strategies or notify users appropriately.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →