By Sagar Shankaran, Founder of CallSphere
Learn how to swap third-party API integrations in AI agent systems without breaking existing workflows. Covers the adapter pattern, interface abstraction, parallel testing, and safe cutover.
Key takeaways
AI agents interact with the world through tool calls. Each tool wraps a third-party API — a CRM, a payment processor, a search engine, a calendar service. When you need to swap Twilio for Vonage, or Stripe for Paddle, or SendGrid for Amazon SES, every agent that uses that tool is affected.
If the tool function is tightly coupled to the vendor SDK, the swap requires changing agent code, rewriting tool definitions, and re-testing every workflow that uses that tool. The adapter pattern eliminates this coupling by putting an abstraction layer between your agents and external APIs.
Start by defining what your agents actually need from the integration, independent of any specific vendor.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
CUR(["On Current Vendor"])
AUDIT["1. Audit current<br/>flows and data"]
EXPORT["2. Export contacts,<br/>scripts, recordings"]
BUILD["3. Build CallSphere<br/>agent and integrations"]
PILOT{"4. Pilot on<br/>10 percent of traffic"}
CUTOVER["5. Forward all<br/>numbers"]
LIVE(["Live on<br/>CallSphere"])
CUR --> AUDIT --> EXPORT --> BUILD --> PILOT
PILOT -->|Pass| CUTOVER --> LIVE
PILOT -->|Issues| BUILD
style CUR fill:#dc2626,stroke:#b91c1c,color:#fff
style PILOT fill:#f59e0b,stroke:#d97706,color:#1f2937
style LIVE fill:#059669,stroke:#047857,color:#fff
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class EmailMessage:
to: str
subject: str
body_html: str
from_address: str
reply_to: Optional[str] = None
@dataclass
class EmailResult:
success: bool
message_id: Optional[str] = None
error: Optional[str] = None
class EmailProvider(ABC):
"""Vendor-agnostic email interface."""
@abstractmethod
async def send(self, message: EmailMessage) -> EmailResult:
...
@abstractmethod
async def check_delivery_status(self, message_id: str) -> str:
...
Each vendor gets its own adapter that implements the interface.
import httpx
class SendGridAdapter(EmailProvider):
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.sendgrid.com/v3"
async def send(self, message: EmailMessage) -> EmailResult:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/mail/send",
headers={"Authorization": f"Bearer {self.api_key}"},
json={
"personalizations": [{"to": [{"email": message.to}]}],
"from": {"email": message.from_address},
"subject": message.subject,
"content": [{
"type": "text/html",
"value": message.body_html,
}],
},
)
if response.status_code == 202:
msg_id = response.headers.get("X-Message-Id", "")
return EmailResult(success=True, message_id=msg_id)
return EmailResult(success=False, error=response.text)
async def check_delivery_status(self, message_id: str) -> str:
# SendGrid status check implementation
return "delivered"
class SESAdapter(EmailProvider):
def __init__(self, region: str = "us-east-1"):
import boto3
self.client = boto3.client("ses", region_name=region)
async def send(self, message: EmailMessage) -> EmailResult:
try:
import asyncio
response = await asyncio.to_thread(
self.client.send_email,
Source=message.from_address,
Destination={"ToAddresses": [message.to]},
Message={
"Subject": {"Data": message.subject},
"Body": {"Html": {"Data": message.body_html}},
},
)
return EmailResult(
success=True,
message_id=response["MessageId"],
)
except Exception as e:
return EmailResult(success=False, error=str(e))
async def check_delivery_status(self, message_id: str) -> str:
return "sent"
The agent tool uses the interface, not the concrete implementation.
from agents import Agent, function_tool, RunContextWrapper
from dataclasses import dataclass
@dataclass
class AppContext:
email_provider: EmailProvider
user_id: str
@function_tool
async def send_email(
wrapper: RunContextWrapper[AppContext],
to: str,
subject: str,
body: str,
) -> str:
"""Send an email to a customer."""
provider = wrapper.context.email_provider
result = await provider.send(EmailMessage(
to=to,
subject=subject,
body_html=body,
from_address="support@example.com",
))
if result.success:
return f"Email sent successfully (ID: {result.message_id})"
return f"Failed to send email: {result.error}"
agent = Agent(
name="Support Agent",
instructions="You help customers with support requests.",
model="gpt-4o",
tools=[send_email],
)
Run both providers simultaneously to verify the new one works before switching.
class ParallelEmailProvider(EmailProvider):
"""Sends through both providers, returns primary result."""
def __init__(
self,
primary: EmailProvider,
shadow: EmailProvider,
):
self.primary = primary
self.shadow = shadow
async def send(self, message: EmailMessage) -> EmailResult:
import asyncio
primary_result, shadow_result = await asyncio.gather(
self.primary.send(message),
self.shadow.send(message),
return_exceptions=True,
)
# Log shadow result for comparison
if isinstance(shadow_result, Exception):
print(f"Shadow provider error: {shadow_result}")
else:
print(f"Shadow result: {shadow_result.success}")
return primary_result # Always return primary
async def check_delivery_status(self, message_id: str) -> str:
return await self.primary.check_delivery_status(message_id)
# During migration testing:
provider = ParallelEmailProvider(
primary=SendGridAdapter(api_key="sg-key"),
shadow=SESAdapter(region="us-east-1"),
)
The actual cutover is a configuration change, not a code change.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
import os
def get_email_provider() -> EmailProvider:
provider_name = os.getenv("EMAIL_PROVIDER", "sendgrid")
if provider_name == "ses":
return SESAdapter(region=os.getenv("AWS_REGION", "us-east-1"))
elif provider_name == "sendgrid":
return SendGridAdapter(api_key=os.environ["SENDGRID_API_KEY"])
else:
raise ValueError(f"Unknown email provider: {provider_name}")
Add optional methods or metadata fields to the interface. For example, if SendGrid supports email scheduling but SES does not, add a schedule_at optional parameter to EmailMessage. The SES adapter ignores it. Document which features are vendor-specific so the team knows what will be lost during migration.
Use it for integrations you might realistically swap: email providers, payment processors, SMS services, and search APIs. Do not over-abstract integrations that are deeply embedded and unlikely to change, like your primary database. The adapter pattern adds indirection — only add it where the flexibility pays off.
For email specifically, use a sandbox mode or test recipient domain. SendGrid and SES both support sandbox endpoints that validate the request without delivering. Set the shadow provider to sandbox mode so you verify API compatibility without spamming users.
#APIMigration #AdapterPattern #Integration #Python #AgentTools #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
CRM integration for the Sales platform: Salesforce + HubSpot pre-wired in CallSphere. Vapi makes you build it. Field mapping, sync, and lead score push.
Modal turns a Python function into autoscaling serverless compute with optional GPU. Deploy a LiveKit Agent with one command and get pay-per-second billing.
Pydantic AI's April release tightens the typed-agent loop and adds structured tool definitions. Why type-safe agents reduce production bugs and speed iteration.
A developer guide to integrating AI voice agents with Salesforce — lead push, call activity logging, and managed packages.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI