API Internals
Internal API implementation documentation for Girard AI
Classification: Internal / God Mode Only Last Updated: January 2026
Overview
Girard AI exposes multiple API surfaces:
- Public API v1 (
/api/v1/*) - External integrations - Internal API (
/api/*) - Dashboard functionality - Widget API (
/api/widget/*) - Embeddable agents - Webhooks (
/api/webhooks/*) - External service callbacks
API Authentication
API Key Authentication
Used for Public API v1 and Widget endpoints.
// lib/api-auth.ts
export async function validateApiKey(request: Request) {
const authHeader = request.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return null
}
const token = authHeader.slice(7)
const keyHash = await hash(token)
const apiKey = await db.apiKey.findUnique({
where: { keyHash },
include: { organization: true }
})
if (!apiKey || !apiKey.isActive || apiKey.expiresAt < new Date()) {
return null
}
// Update usage stats
await db.apiKey.update({
where: { id: apiKey.id },
data: { lastUsedAt: new Date(), usageCount: { increment: 1 } }
})
return apiKey
}
Session Authentication
Used for Internal API endpoints.
// Using Clerk
import { auth } from '@clerk/nextjs/server'
export async function GET(request: Request) {
const { userId, orgId } = await auth()
if (!userId) {
return new Response('Unauthorized', { status: 401 })
}
// ...
}
Public API v1
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/workflows | List workflows |
| GET | /api/v1/workflows/:id | Get workflow |
| POST | /api/v1/workflows/:id/execute | Execute workflow |
| GET | /api/v1/workflows/:id/runs | List runs |
| GET | /api/v1/runs/:id | Get run status |
Request/Response Format
// Request
POST /api/v1/workflows/:id/execute
Authorization: Bearer gai_sk_...
Content-Type: application/json
{
"input": {
"prompt": "Generate a summary",
"data": { ... }
}
}
// Response
{
"success": true,
"data": {
"runId": "run_abc123",
"status": "RUNNING",
"startedAt": "2026-01-28T12:00:00Z"
}
}
Rate Limiting
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
export const apiRateLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'),
prefix: 'api:rate',
})
// Usage in route
const { success, limit, remaining } = await apiRateLimiter.limit(apiKey.id)
if (!success) {
return new Response('Rate limit exceeded', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
}
})
}
Internal API
Server Actions
Preferred for mutations from the UI.
// app/actions/workflows.ts
'use server'
import { auth } from '@clerk/nextjs/server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
const CreateWorkflowSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().optional(),
})
export async function createWorkflow(input: unknown) {
const { userId, orgId } = await auth()
if (!userId) throw new Error('Unauthorized')
const data = CreateWorkflowSchema.parse(input)
const workflow = await db.workflow.create({
data: {
...data,
userId,
organizationId: orgId,
}
})
revalidatePath('/workflows')
return { success: true, workflowId: workflow.id }
}
API Routes
Used for complex operations or streaming.
// app/api/chat/route.ts
import { StreamingTextResponse, AIStream } from 'ai'
export async function POST(request: Request) {
const { messages, model } = await request.json()
const response = await anthropic.messages.stream({
model,
messages,
stream: true,
})
return new StreamingTextResponse(AIStream(response))
}
Webhook Handlers
Clerk Webhooks
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix'
export async function POST(request: Request) {
const payload = await request.text()
const headers = {
'svix-id': request.headers.get('svix-id')!,
'svix-timestamp': request.headers.get('svix-timestamp')!,
'svix-signature': request.headers.get('svix-signature')!,
}
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!)
const event = wh.verify(payload, headers)
switch (event.type) {
case 'user.created':
await handleUserCreated(event.data)
break
case 'user.updated':
await handleUserUpdated(event.data)
break
case 'organization.created':
await handleOrgCreated(event.data)
break
}
return new Response('OK')
}
Stripe Webhooks
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: Request) {
const payload = await request.text()
const sig = request.headers.get('stripe-signature')!
const event = stripe.webhooks.constructEvent(
payload,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
switch (event.type) {
case 'customer.subscription.created':
case 'customer.subscription.updated':
await handleSubscriptionChange(event.data.object)
break
case 'invoice.paid':
await handleInvoicePaid(event.data.object)
break
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object)
break
}
return new Response('OK')
}
Twilio Webhooks
// app/api/webhooks/twilio/voice/route.ts
import twilio from 'twilio'
export async function POST(request: Request) {
const formData = await request.formData()
const callSid = formData.get('CallSid')
const from = formData.get('From')
const to = formData.get('To')
// Validate Twilio signature
const signature = request.headers.get('X-Twilio-Signature')
const isValid = twilio.validateRequest(
process.env.TWILIO_AUTH_TOKEN!,
signature!,
request.url,
Object.fromEntries(formData)
)
if (!isValid) {
return new Response('Invalid signature', { status: 403 })
}
// Find associated voice agent
const phoneNumber = await db.phoneNumber.findUnique({
where: { number: to },
include: { voiceAgent: true }
})
// Generate TwiML response
const twiml = new twilio.twiml.VoiceResponse()
twiml.say({ voice: phoneNumber.voiceAgent.voiceId },
phoneNumber.voiceAgent.greeting)
return new Response(twiml.toString(), {
headers: { 'Content-Type': 'text/xml' }
})
}
Error Handling
Standard Error Response
// lib/api-response.ts
export function apiError(
message: string,
status: number = 400,
code?: string
) {
return new Response(
JSON.stringify({
success: false,
error: message,
code,
}),
{
status,
headers: { 'Content-Type': 'application/json' }
}
)
}
export function apiSuccess<T>(data: T, status: number = 200) {
return new Response(
JSON.stringify({
success: true,
data,
}),
{
status,
headers: { 'Content-Type': 'application/json' }
}
)
}
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid authentication |
FORBIDDEN | 403 | Insufficient permissions |
NOT_FOUND | 404 | Resource not found |
RATE_LIMITED | 429 | Too many requests |
INSUFFICIENT_CREDITS | 402 | Not enough credits |
VALIDATION_ERROR | 400 | Invalid input |
INTERNAL_ERROR | 500 | Server error |
Credit Integration
Checking Credits
// Before expensive operations
const creditCheck = await checkCredits(
orgId,
'AI_CHAT',
model,
estimatedTokens
)
if (!creditCheck.allowed) {
return apiError(
'Insufficient credits',
402,
'INSUFFICIENT_CREDITS'
)
}
Deducting Credits
// After successful operation
await deductCredits(orgId, {
amount: actualCreditsUsed,
type: 'USAGE',
featureType: 'AI_CHAT',
description: `Chat completion with ${model}`,
referenceId: completionId,
})
Logging API Costs
// Track actual provider costs
await logApiCost({
organizationId: orgId,
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
operation: 'chat',
inputUnits: inputTokens / 1000,
outputUnits: outputTokens / 1000,
creditsCharged: creditsUsed,
})
Widget API
CORS Configuration
// app/api/widget/[token]/route.ts
const ALLOWED_ORIGINS = ['*'] // Embeddable anywhere
export async function OPTIONS(request: Request) {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
}
})
}
export async function POST(
request: Request,
{ params }: { params: { token: string } }
) {
// Validate widget token
const agent = await db.chatAgent.findUnique({
where: { widgetToken: params.token, isActive: true }
})
if (!agent) {
return apiError('Invalid widget token', 401)
}
// Process chat message
// ...
return new Response(response, {
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
}
})
}
Testing APIs
Unit Tests
// __tests__/api/workflows.test.ts
import { describe, it, expect, vi } from 'vitest'
vi.mock('@clerk/nextjs/server', () => ({
auth: vi.fn().mockResolvedValue({ userId: 'user-123', orgId: 'org-123' })
}))
describe('Workflow API', () => {
it('creates workflow', async () => {
const result = await createWorkflow({
name: 'Test Workflow',
description: 'Test description',
})
expect(result.success).toBe(true)
expect(result.workflowId).toBeDefined()
})
})
E2E Tests
// e2e/api.spec.ts
import { test, expect } from '@playwright/test'
test('API v1 workflow execution', async ({ request }) => {
const response = await request.post('/api/v1/workflows/wf_123/execute', {
headers: {
'Authorization': `Bearer ${process.env.TEST_API_KEY}`,
},
data: {
input: { prompt: 'Test prompt' }
}
})
expect(response.ok()).toBeTruthy()
const data = await response.json()
expect(data.success).toBe(true)
expect(data.data.runId).toBeDefined()
})
Performance Considerations
Response Streaming
For long-running AI operations:
export async function POST(request: Request) {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of aiStream) {
controller.enqueue(encoder.encode(chunk))
}
controller.close()
}
})
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' }
})
}
Caching
// Cache GET responses
export async function GET(request: Request) {
const cached = await redis.get(cacheKey)
if (cached) {
return apiSuccess(JSON.parse(cached))
}
const data = await fetchData()
await redis.setex(cacheKey, 300, JSON.stringify(data))
return apiSuccess(data)
}
Pagination
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100)
const [items, total] = await Promise.all([
db.item.findMany({
skip: (page - 1) * limit,
take: limit,
}),
db.item.count()
])
return apiSuccess({
items,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
}
})
}