Claude-skill-registry external-integration-patterns
Patterns for reliable external service integration: env validation, health checks, error handling, observability. Invoke when integrating Stripe, Clerk, Sendgrid, or any external API.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/external-integration-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-external-integration-patterns && rm -rf "$T"
skills/data/external-integration-patterns/SKILL.mdExternal Integration Patterns
Patterns for reliable external service integration.
Triggers
Invoke this skill when:
- File path contains
,webhook
,api/services/ - Code imports external service SDKs (stripe, @clerk, @sendgrid, etc.)
- Env vars reference external services
- Implementing any third-party API integration
- Reviewing webhook handlers
Core Principle
External services fail. Your integration must be observable, recoverable, and fail loudly.
Silent failures are the worst failures. When Stripe doesn't deliver a webhook, when Clerk JWT validation fails, when Sendgrid rejects an email — you need to know immediately, not when a user complains.
Required Patterns
1. Fail-Fast Env Validation
Validate environment variables at module load, not at runtime. Fail immediately with a clear message.
// At module load, NOT inside a function const REQUIRED = ['SERVICE_API_KEY', 'SERVICE_WEBHOOK_SECRET']; for (const key of REQUIRED) { const value = process.env[key]; if (!value) { throw new Error(`Missing required env var: ${key}`); } if (value !== value.trim()) { throw new Error(`${key} has trailing whitespace — check dashboard for invisible characters`); } } // Now safe to use export const apiKey = process.env.SERVICE_API_KEY!;
Why this matters:
- Deploy fails immediately if config is wrong
- Error message tells you exactly what's missing
- No silent failures at 3am when a customer tries to checkout
2. Health Check Endpoint
Every external service should have a health check endpoint.
// /api/health/route.ts or /api/health/[service]/route.ts export async function GET() { const checks: Record<string, { ok: boolean; latency?: number; error?: string }> = {}; // Check Stripe try { const start = Date.now(); await stripe.balance.retrieve(); checks.stripe = { ok: true, latency: Date.now() - start }; } catch (e) { checks.stripe = { ok: false, error: e.message }; } // Check database try { const start = Date.now(); await db.query.users.findFirst(); checks.database = { ok: true, latency: Date.now() - start }; } catch (e) { checks.database = { ok: false, error: e.message }; } const healthy = Object.values(checks).every(c => c.ok); return Response.json({ status: healthy ? 'ok' : 'degraded', checks, timestamp: new Date().toISOString() }, { status: healthy ? 200 : 503 }); }
3. Structured Error Logging
Log every external service failure with full context.
catch (error) { // Structured JSON for log aggregation console.error(JSON.stringify({ level: 'error', service: 'stripe', operation: 'createCheckout', userId: user.id, input: { priceId, mode }, // Safe subset of input error: error.message, code: error.code || 'unknown', timestamp: new Date().toISOString() })); throw error; }
Required fields:
: Which external service (stripe, clerk, sendgrid)service
: What you were trying to dooperation
: Who this affects (for debugging)userId
: The error messageerror
: When it happenedtimestamp
4. Webhook Reliability
Webhooks are inherently unreliable. Build for this reality.
export async function handleWebhook(req: Request) { const body = await req.text(); const sig = req.headers.get('stripe-signature')!; // 1. Verify signature FIRST (before any processing) let event: Stripe.Event; try { event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!); } catch (e) { console.error(JSON.stringify({ level: 'error', source: 'webhook', service: 'stripe', error: 'Signature verification failed', message: e.message })); return new Response('Invalid signature', { status: 400 }); } // 2. Log event received BEFORE processing console.log(JSON.stringify({ level: 'info', source: 'webhook', service: 'stripe', eventType: event.type, eventId: event.id, timestamp: new Date().toISOString() })); // 3. Store event for reconciliation (optional but recommended) await db.insert(webhookEvents).values({ provider: 'stripe', eventId: event.id, eventType: event.type, payload: event, processedAt: null }); // 4. Return 200 quickly, process async if slow // (Stripe retries if response takes too long) await processEvent(event); return new Response('OK', { status: 200 }); }
5. Reconciliation Cron (Safety Net)
Don't rely 100% on webhooks. Periodically sync state as a backup.
// Run hourly or daily export async function reconcileSubscriptions() { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // Fetch active subscriptions modified in last 24h const subs = await stripe.subscriptions.list({ status: 'active', created: { gte: Math.floor(Date.now() / 1000) - 86400 } }); for (const sub of subs.data) { // Update local state to match Stripe await db.update(subscriptions) .set({ status: sub.status, currentPeriodEnd: sub.current_period_end }) .where(eq(subscriptions.stripeId, sub.id)); } console.log(JSON.stringify({ level: 'info', operation: 'reconcileSubscriptions', synced: subs.data.length, timestamp: new Date().toISOString() })); }
6. Pull-on-Success Activation
Don't wait for webhook to grant access. Verify payment immediately after redirect.
// /checkout/success/page.tsx export default async function SuccessPage({ searchParams }) { const sessionId = searchParams.session_id; // Don't trust the URL alone — verify with Stripe const session = await stripe.checkout.sessions.retrieve(sessionId); if (session.payment_status === 'paid') { // Grant access immediately await grantAccess(session.customer); } // Webhook will come later as backup return <SuccessMessage />; }
Pre-Deploy Checklist
Before deploying any external integration:
Environment Variables
- All required vars in
.env.example - Vars set on both dev and prod deployments
- No trailing whitespace (use
, notprintf
)echo - Format validated (sk_, whsec_, pk_*)
Webhook Configuration
- Webhook URL uses canonical domain (no redirects)
- Secret matches between service dashboard and env vars
- Signature verification in handler
- Events logged before processing
Observability
- Health check endpoint exists
- Error paths log with context
- Monitoring/alerting configured
Reliability
- Reconciliation cron or pull-on-success pattern
- Idempotency for duplicate events
- Graceful handling of service downtime
Quick Verification Script
#!/bin/bash # scripts/verify-external-integration.sh SERVICE=$1 echo "Checking $SERVICE integration..." # Check env vars for var in ${SERVICE}_API_KEY ${SERVICE}_WEBHOOK_SECRET; do if [ -z "${!var}" ]; then echo "❌ Missing $var" exit 1 fi if [ "${!var}" != "$(echo "${!var}" | tr -d '\n')" ]; then echo "❌ $var has trailing newline" exit 1 fi done # Check health endpoint HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/health) if [ "$HTTP_CODE" != "200" ]; then echo "❌ Health check failed (HTTP $HTTP_CODE)" exit 1 fi echo "✅ $SERVICE integration checks passed"
Anti-Patterns to Avoid
// ❌ BAD: Silent failure on missing config const apiKey = process.env.API_KEY || ''; // ❌ BAD: No context in error log catch (e) { console.log('Error'); throw e; } // ❌ BAD: Trusting webhook without verification const event = JSON.parse(body); // No signature check! // ❌ BAD: 100% reliance on webhooks // If webhook fails, user never gets access // ❌ BAD: No logging of received events // Debugging nightmare when things go wrong
Service-Specific Notes
Stripe
- Use
for signature verificationstripe.webhooks.constructEvent() - Check Stripe Dashboard > Developers > Webhooks for delivery logs
param only valid incustomer_creation
/payment
modesetup
Clerk
must match exactly between Clerk and ConvexCONVEX_WEBHOOK_TOKEN- JWT template names are case-sensitive
- Webhook URL must not redirect
Sendgrid
- Verify sender domain before going live
- Inbound parse webhooks need signature verification
- Rate limits apply — implement queuing for bulk sends