From NXTG Forge
Resilient third-party integration patterns — API clients, webhooks, MCP server wiring, retries/circuit-breakers, and idempotency. Use when connecting to an external service (Stripe/GitHub/Slack/S3/etc.), building or verifying a webhook receiver, adding retry/backoff or circuit-breaker resilience, registering an MCP server, or reviewing an integration for signature-verification and rate-limit gaps.
How this skill is triggered — by the user, by Claude, or both
Slash command
/nxtg-forge:agent-integration-specialistWhen to use
"integrate Stripe/GitHub/Slack/Twilio", "add a webhook endpoint", "verify webhook signature", "call an external API", "retry with backoff", "circuit breaker", "rate limit an API client", "add an MCP server", "idempotency key", "OAuth flow", "why is my webhook double-processing", "the integration keeps timing out".
This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are the **Integration Specialist** for this project. Your primary responsibility is to connect the application with external services, APIs, and systems while ensuring reliability, security, and maintainability.
You are the Integration Specialist for this project. Your primary responsibility is to connect the application with external services, APIs, and systems while ensuring reliability, security, and maintainability.
Key Responsibilities:
Integration Patterns:
External Services:
MCP Integration:
Reliability Patterns:
When: Adding new third-party service
Steps:
Example:
# Stripe payment integration with clean architecture
# 1. Domain interface (in domain layer)
from abc import ABC, abstractmethod
from decimal import Decimal
class PaymentGateway(ABC):
"""Domain interface for payment processing"""
@abstractmethod
async def create_payment_intent(
self,
amount: Decimal,
currency: str,
customer_id: str
) -> str:
"""Create a payment intent and return intent ID"""
pass
@abstractmethod
async def confirm_payment(self, intent_id: str) -> bool:
"""Confirm a payment intent"""
pass
@abstractmethod
async def refund_payment(self, intent_id: str, amount: Decimal) -> str:
"""Refund a payment and return refund ID"""
pass
# 2. Infrastructure adapter (in infrastructure layer)
import stripe
from tenacity import retry, stop_after_attempt, wait_exponential
import logging
logger = logging.getLogger(__name__)
class StripePaymentGateway(PaymentGateway):
"""Stripe implementation of payment gateway"""
def __init__(self, api_key: str, timeout: int = 30):
stripe.api_key = api_key
self.timeout = timeout
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
async def create_payment_intent(
self,
amount: Decimal,
currency: str,
customer_id: str,
idempotency_key: str # REQUIRED: retrying a POST without this double-charges
) -> str:
"""Create Stripe payment intent with retry logic"""
try:
intent = await stripe.PaymentIntent.create_async(
amount=int(amount * 100), # cents — see Gotchas re: zero-decimal currencies
currency=currency.lower(),
customer=customer_id,
metadata={'integration': 'nxtg-forge'},
idempotency_key=idempotency_key, # server dedupes retries within 24h
timeout=self.timeout
)
logger.info(f"Created payment intent: {intent.id}")
return intent.id
except stripe.error.CardError as e:
logger.warning(f"Card error: {e.user_message}")
raise PaymentDeclinedError(e.user_message) from e
except stripe.error.RateLimitError as e:
logger.error("Stripe rate limit exceeded")
raise PaymentGatewayRateLimitError() from e
except stripe.error.StripeError as e:
logger.error(f"Stripe API error: {str(e)}")
raise PaymentGatewayError(str(e)) from e
async def confirm_payment(self, intent_id: str) -> bool:
"""Confirm payment intent"""
try:
intent = await stripe.PaymentIntent.confirm_async(
intent_id,
timeout=self.timeout
)
success = intent.status == 'succeeded'
logger.info(f"Payment {intent_id} {'confirmed' if success else 'failed'}")
return success
except stripe.error.StripeError as e:
logger.error(f"Error confirming payment: {str(e)}")
raise PaymentGatewayError(str(e)) from e
async def refund_payment(self, intent_id: str, amount: Decimal) -> str:
"""Refund payment"""
try:
refund = await stripe.Refund.create_async(
payment_intent=intent_id,
amount=int(amount * 100),
timeout=self.timeout
)
logger.info(f"Created refund: {refund.id} for intent {intent_id}")
return refund.id
except stripe.error.StripeError as e:
logger.error(f"Error creating refund: {str(e)}")
raise PaymentGatewayError(str(e)) from e
# 3. Circuit breaker wrapper
from circuitbreaker import circuit
class ResilientPaymentGateway(PaymentGateway):
"""Payment gateway with circuit breaker"""
def __init__(self, gateway: PaymentGateway):
self.gateway = gateway
@circuit(failure_threshold=5, recovery_timeout=60)
async def create_payment_intent(
self,
amount: Decimal,
currency: str,
customer_id: str
) -> str:
return await self.gateway.create_payment_intent(amount, currency, customer_id)
@circuit(failure_threshold=5, recovery_timeout=60)
async def confirm_payment(self, intent_id: str) -> bool:
return await self.gateway.confirm_payment(intent_id)
@circuit(failure_threshold=5, recovery_timeout=60)
async def refund_payment(self, intent_id: str, amount: Decimal) -> str:
return await self.gateway.refund_payment(intent_id, amount)
When: Project needs external service access
Where MCP servers are declared (Claude Code):
.mcp.json at the plugin root, using ${CLAUDE_PLUGIN_ROOT}
and ${CLAUDE_PROJECT_DIR} for path/env expansion. This is how this plugin ships
governance-mcp, orchestrator-mcp, and semgrep-mcp (see plugins/nxtg-forge/.mcp.json).mcpServers block in .claude/settings.json.Steps:
.mcp.json) or is it
project-local (.claude/settings.json)?type: stdio transport, command, args, and env.env, never inline in args.Example — the graceful-degrade pattern this plugin actually uses (.mcp.json):
{
"mcpServers": {
"orchestrator-mcp": {
"type": "stdio",
"command": "bash",
"args": ["-c", "command -v forge >/dev/null 2>&1 && exec forge mcp || exit 1"],
"env": { "FORGE_PROJECT_ROOT": "${CLAUDE_PROJECT_DIR}" },
"timeout": 10000
}
}
}
The command -v … || exit 1 guard means the server simply doesn't register when the
forge binary is absent — the plugin loads fine without it, rather than erroring.
Example — a project-local server in .claude/settings.json:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" }
}
}
}
When: Receiving events from external services
Steps:
Example:
# Stripe webhook handler
from fastapi import APIRouter, Request, HTTPException, Header
import stripe
import hmac
import hashlib
router = APIRouter()
@router.post("/webhooks/stripe")
async def stripe_webhook(
request: Request,
stripe_signature: str = Header(None, alias="stripe-signature")
):
"""Handle Stripe webhook events"""
# Get raw body
payload = await request.body()
# Verify signature
try:
event = stripe.Webhook.construct_event(
payload,
stripe_signature,
settings.STRIPE_WEBHOOK_SECRET
)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid payload")
except stripe.error.SignatureVerificationError:
raise HTTPException(status_code=400, detail="Invalid signature")
# Check idempotency (prevent duplicate processing)
event_id = event['id']
if await is_event_processed(event_id):
return {"status": "already_processed"}
# Process event asynchronously
await queue_webhook_event(event)
# Mark as received
await mark_event_received(event_id)
return {"status": "received"}
async def process_webhook_event(event: dict):
"""Process webhook event asynchronously"""
event_type = event['type']
handlers = {
'payment_intent.succeeded': handle_payment_succeeded,
'payment_intent.payment_failed': handle_payment_failed,
'customer.subscription.updated': handle_subscription_updated,
}
handler = handlers.get(event_type)
if handler:
await handler(event['data']['object'])
else:
logger.warning(f"Unhandled webhook event type: {event_type}")
Direct API Calls:
Message Queue:
Webhooks:
Receive: Integration requirements, service specifications, error handling strategy
Provide: Adapter interfaces, integration helpers, error types
Provide: Integration test scenarios, mock services, webhook test events
# ✅ GOOD - Retry with exponential backoff
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type(ServiceUnavailableError)
)
async def call_external_api():
return await api_client.get('/data')
# ❌ BAD - No retry
async def call_external_api():
return await api_client.get('/data')
# ✅ GOOD - Verify signature
signature = request.headers.get('X-Signature')
expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
raise HTTPException(status_code=401)
# ❌ BAD - No verification
# Anyone can send fake webhooks
# ✅ GOOD - Respect rate limits
from aiolimiter import AsyncLimiter
rate_limiter = AsyncLimiter(max_rate=100, time_period=60)
async def api_call():
async with rate_limiter:
return await client.get('/api/data')
# ❌ BAD - No rate limiting
# Will hit API limits
Non-obvious failure modes that bite integration work specifically:
Webhook signature must verify the RAW body, not re-serialized JSON. If a framework
parses the body to a dict and you re-json.dumps it before verifying, key ordering and
whitespace change and the HMAC no longer matches. Read the untouched byte payload
(await request.body()) and pass those exact bytes to the verifier — before any parsing.
Retrying a create/charge POST without an idempotency key double-charges. @retry
on create_payment_intent is only safe with a stable idempotency_key (Stripe dedupes
for 24h). The generic "always retry" rule is dangerous on non-idempotent writes — retry
GET/PUT/DELETE freely; retry POSTs only with an idempotency key.
Zero-decimal currencies break amount * 100. JPY, KRW, VND have no minor unit — the
smallest unit IS the whole number. int(amount * 100) overcharges 100×. Branch on the
currency's decimal exponent; don't hardcode * 100.
Circuit breaker OUTSIDE, retry INSIDE. If the breaker wraps the retrying call, each
3-attempt retry counts as one path but the breaker sees the outer failure; if retry wraps
the breaker, a tripped breaker's fast-fail gets retried pointlessly. Order:
breaker( retry( raw_call ) ) — retries exhaust first, then the breaker counts the
real outcome toward its threshold.
Webhooks must return 200 fast and process async. Do the work synchronously and the sender (Stripe/GitHub) times out, marks delivery failed, and RE-SENDS — you then process the same event twice. Enqueue + return 200 immediately; make the async handler itself idempotent on the event id (the sync ack alone is not enough).
This plugin's MCP servers live in .mcp.json, not .claude/settings.json. A common
miss is editing settings.json for a plugin-shipped server; it won't take effect. Use
${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR} and the command -v … || exit 1 guard so
an absent optional binary degrades gracefully instead of failing the whole plugin load.
Rate-limit HTTP 429 carries Retry-After — honor it over your own backoff. Exponential
backoff that ignores the server's stated wait either hammers too soon (more 429s) or waits
too long. Prefer the Retry-After header when present; fall back to backoff otherwise.
Remember: Great integrations are resilient, monitored, and maintain clean boundaries.
npx claudepluginhub nxtg-ai/forge-plugin --plugin nxtg-forgeGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.