KS
Killer-Skills

convex-http-actions — Categories.community

v1.0.0
GitHub

About this Skill

Perfect for Full Stack Agents needing to build production-ready Convex applications with custom HTTP endpoints AI agent skills and templates for building production ready apps with Convex. Patterns for queries, mutations, cron jobs, webhooks, migrations, and more.

waynesutton waynesutton
[0]
[0]
Updated: 3/4/2026

Quality Score

Top 5%
70
Excellent
Based on code quality & docs
Installation
SYS Universal Install (Auto-Detect)
Cursor IDE Windsurf IDE VS Code IDE
> npx killer-skills add waynesutton/convexskills

Agent Capability Analysis

The convex-http-actions MCP Server by waynesutton is an open-source Categories.community integration for Claude and other AI agents, enabling seamless task automation and capability expansion.

Ideal Agent Persona

Perfect for Full Stack Agents needing to build production-ready Convex applications with custom HTTP endpoints

Core Value

Empowers agents to build HTTP endpoints for webhooks, external API integrations, and custom routes in Convex applications using Convex HTTP Actions, supporting features like queries, mutations, cron jobs, and authentication protocols

Capabilities Granted for convex-http-actions MCP Server

Building custom webhooks for real-time data processing
Integrating external APIs for enhanced application functionality
Creating cron jobs for scheduled task automation

! Prerequisites & Limits

  • Requires Convex platform access
  • Dependent on Convex documentation for implementation details
Project
SKILL.md
17.7 KB
.cursorrules
1.2 KB
package.json
240 B
Ready
UTF-8

# Tags

[No tags]
SKILL.md
Readonly

Convex HTTP Actions

Build HTTP endpoints for webhooks, external API integrations, and custom routes in Convex applications.

Documentation Sources

Before implementing, do not assume; fetch the latest documentation:

Instructions

HTTP Actions Overview

HTTP actions allow you to define HTTP endpoints in Convex that can:

  • Receive webhooks from third-party services
  • Create custom API routes
  • Handle file uploads
  • Integrate with external services
  • Serve dynamic content

Basic HTTP Router Setup

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4 5const http = httpRouter(); 6 7// Simple GET endpoint 8http.route({ 9 path: "/health", 10 method: "GET", 11 handler: httpAction(async (ctx, request) => { 12 return new Response(JSON.stringify({ status: "ok" }), { 13 status: 200, 14 headers: { "Content-Type": "application/json" }, 15 }); 16 }), 17}); 18 19export default http;

Request Handling

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4 5const http = httpRouter(); 6 7// Handle JSON body 8http.route({ 9 path: "/api/data", 10 method: "POST", 11 handler: httpAction(async (ctx, request) => { 12 // Parse JSON body 13 const body = await request.json(); 14 15 // Access headers 16 const authHeader = request.headers.get("Authorization"); 17 18 // Access URL parameters 19 const url = new URL(request.url); 20 const queryParam = url.searchParams.get("filter"); 21 22 return new Response( 23 JSON.stringify({ received: body, filter: queryParam }), 24 { 25 status: 200, 26 headers: { "Content-Type": "application/json" }, 27 } 28 ); 29 }), 30}); 31 32// Handle form data 33http.route({ 34 path: "/api/form", 35 method: "POST", 36 handler: httpAction(async (ctx, request) => { 37 const formData = await request.formData(); 38 const name = formData.get("name"); 39 const email = formData.get("email"); 40 41 return new Response( 42 JSON.stringify({ name, email }), 43 { 44 status: 200, 45 headers: { "Content-Type": "application/json" }, 46 } 47 ); 48 }), 49}); 50 51// Handle raw bytes 52http.route({ 53 path: "/api/upload", 54 method: "POST", 55 handler: httpAction(async (ctx, request) => { 56 const bytes = await request.bytes(); 57 const contentType = request.headers.get("Content-Type") ?? "application/octet-stream"; 58 59 // Store in Convex storage 60 const blob = new Blob([bytes], { type: contentType }); 61 const storageId = await ctx.storage.store(blob); 62 63 return new Response( 64 JSON.stringify({ storageId }), 65 { 66 status: 200, 67 headers: { "Content-Type": "application/json" }, 68 } 69 ); 70 }), 71}); 72 73export default http;

Path Parameters

Use path prefix matching for dynamic routes:

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4 5const http = httpRouter(); 6 7// Match /api/users/* with pathPrefix 8http.route({ 9 pathPrefix: "/api/users/", 10 method: "GET", 11 handler: httpAction(async (ctx, request) => { 12 const url = new URL(request.url); 13 // Extract user ID from path: /api/users/123 -> "123" 14 const userId = url.pathname.replace("/api/users/", ""); 15 16 return new Response( 17 JSON.stringify({ userId }), 18 { 19 status: 200, 20 headers: { "Content-Type": "application/json" }, 21 } 22 ); 23 }), 24}); 25 26export default http;

CORS Configuration

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4 5const http = httpRouter(); 6 7// CORS headers helper 8const corsHeaders = { 9 "Access-Control-Allow-Origin": "*", 10 "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", 11 "Access-Control-Allow-Headers": "Content-Type, Authorization", 12 "Access-Control-Max-Age": "86400", 13}; 14 15// Handle preflight requests 16http.route({ 17 path: "/api/data", 18 method: "OPTIONS", 19 handler: httpAction(async () => { 20 return new Response(null, { 21 status: 204, 22 headers: corsHeaders, 23 }); 24 }), 25}); 26 27// Actual endpoint with CORS 28http.route({ 29 path: "/api/data", 30 method: "POST", 31 handler: httpAction(async (ctx, request) => { 32 const body = await request.json(); 33 34 return new Response( 35 JSON.stringify({ success: true, data: body }), 36 { 37 status: 200, 38 headers: { 39 "Content-Type": "application/json", 40 ...corsHeaders, 41 }, 42 } 43 ); 44 }), 45}); 46 47export default http;

Webhook Handling

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4import { internal } from "./_generated/api"; 5 6const http = httpRouter(); 7 8// Stripe webhook 9http.route({ 10 path: "/webhooks/stripe", 11 method: "POST", 12 handler: httpAction(async (ctx, request) => { 13 const signature = request.headers.get("stripe-signature"); 14 if (!signature) { 15 return new Response("Missing signature", { status: 400 }); 16 } 17 18 const body = await request.text(); 19 20 // Verify webhook signature (in action with Node.js) 21 try { 22 await ctx.runAction(internal.stripe.verifyAndProcessWebhook, { 23 body, 24 signature, 25 }); 26 return new Response("OK", { status: 200 }); 27 } catch (error) { 28 console.error("Webhook error:", error); 29 return new Response("Webhook error", { status: 400 }); 30 } 31 }), 32}); 33 34// GitHub webhook 35http.route({ 36 path: "/webhooks/github", 37 method: "POST", 38 handler: httpAction(async (ctx, request) => { 39 const event = request.headers.get("X-GitHub-Event"); 40 const signature = request.headers.get("X-Hub-Signature-256"); 41 42 if (!signature) { 43 return new Response("Missing signature", { status: 400 }); 44 } 45 46 const body = await request.text(); 47 48 await ctx.runAction(internal.github.processWebhook, { 49 event: event ?? "unknown", 50 body, 51 signature, 52 }); 53 54 return new Response("OK", { status: 200 }); 55 }), 56}); 57 58export default http;

Webhook Signature Verification

typescript
1// convex/stripe.ts 2"use node"; 3 4import { internalAction, internalMutation } from "./_generated/server"; 5import { internal } from "./_generated/api"; 6import { v } from "convex/values"; 7import Stripe from "stripe"; 8 9const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); 10 11export const verifyAndProcessWebhook = internalAction({ 12 args: { 13 body: v.string(), 14 signature: v.string(), 15 }, 16 returns: v.null(), 17 handler: async (ctx, args) => { 18 const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!; 19 20 // Verify signature 21 const event = stripe.webhooks.constructEvent( 22 args.body, 23 args.signature, 24 webhookSecret 25 ); 26 27 // Process based on event type 28 switch (event.type) { 29 case "checkout.session.completed": 30 await ctx.runMutation(internal.payments.handleCheckoutComplete, { 31 sessionId: event.data.object.id, 32 customerId: event.data.object.customer as string, 33 }); 34 break; 35 36 case "customer.subscription.updated": 37 await ctx.runMutation(internal.subscriptions.handleUpdate, { 38 subscriptionId: event.data.object.id, 39 status: event.data.object.status, 40 }); 41 break; 42 } 43 44 return null; 45 }, 46});

Authentication in HTTP Actions

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4import { internal } from "./_generated/api"; 5 6const http = httpRouter(); 7 8// API key authentication 9http.route({ 10 path: "/api/protected", 11 method: "GET", 12 handler: httpAction(async (ctx, request) => { 13 const apiKey = request.headers.get("X-API-Key"); 14 15 if (!apiKey) { 16 return new Response( 17 JSON.stringify({ error: "Missing API key" }), 18 { status: 401, headers: { "Content-Type": "application/json" } } 19 ); 20 } 21 22 // Validate API key 23 const isValid = await ctx.runQuery(internal.auth.validateApiKey, { 24 apiKey, 25 }); 26 27 if (!isValid) { 28 return new Response( 29 JSON.stringify({ error: "Invalid API key" }), 30 { status: 403, headers: { "Content-Type": "application/json" } } 31 ); 32 } 33 34 // Process authenticated request 35 const data = await ctx.runQuery(internal.data.getProtectedData, {}); 36 37 return new Response( 38 JSON.stringify(data), 39 { status: 200, headers: { "Content-Type": "application/json" } } 40 ); 41 }), 42}); 43 44// Bearer token authentication 45http.route({ 46 path: "/api/user", 47 method: "GET", 48 handler: httpAction(async (ctx, request) => { 49 const authHeader = request.headers.get("Authorization"); 50 51 if (!authHeader?.startsWith("Bearer ")) { 52 return new Response( 53 JSON.stringify({ error: "Missing or invalid Authorization header" }), 54 { status: 401, headers: { "Content-Type": "application/json" } } 55 ); 56 } 57 58 const token = authHeader.slice(7); 59 60 // Validate token and get user 61 const user = await ctx.runQuery(internal.auth.validateToken, { token }); 62 63 if (!user) { 64 return new Response( 65 JSON.stringify({ error: "Invalid token" }), 66 { status: 403, headers: { "Content-Type": "application/json" } } 67 ); 68 } 69 70 return new Response( 71 JSON.stringify(user), 72 { status: 200, headers: { "Content-Type": "application/json" } } 73 ); 74 }), 75}); 76 77export default http;

Calling Mutations and Queries

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4import { api, internal } from "./_generated/api"; 5 6const http = httpRouter(); 7 8http.route({ 9 path: "/api/items", 10 method: "POST", 11 handler: httpAction(async (ctx, request) => { 12 const body = await request.json(); 13 14 // Call a mutation 15 const itemId = await ctx.runMutation(internal.items.create, { 16 name: body.name, 17 description: body.description, 18 }); 19 20 // Query the created item 21 const item = await ctx.runQuery(internal.items.get, { id: itemId }); 22 23 return new Response( 24 JSON.stringify(item), 25 { status: 201, headers: { "Content-Type": "application/json" } } 26 ); 27 }), 28}); 29 30http.route({ 31 path: "/api/items", 32 method: "GET", 33 handler: httpAction(async (ctx, request) => { 34 const url = new URL(request.url); 35 const limit = parseInt(url.searchParams.get("limit") ?? "10"); 36 37 const items = await ctx.runQuery(internal.items.list, { limit }); 38 39 return new Response( 40 JSON.stringify(items), 41 { status: 200, headers: { "Content-Type": "application/json" } } 42 ); 43 }), 44}); 45 46export default http;

Error Handling

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4 5const http = httpRouter(); 6 7// Helper for JSON responses 8function jsonResponse(data: unknown, status = 200) { 9 return new Response(JSON.stringify(data), { 10 status, 11 headers: { "Content-Type": "application/json" }, 12 }); 13} 14 15// Helper for error responses 16function errorResponse(message: string, status: number) { 17 return jsonResponse({ error: message }, status); 18} 19 20http.route({ 21 path: "/api/process", 22 method: "POST", 23 handler: httpAction(async (ctx, request) => { 24 try { 25 // Validate content type 26 const contentType = request.headers.get("Content-Type"); 27 if (!contentType?.includes("application/json")) { 28 return errorResponse("Content-Type must be application/json", 415); 29 } 30 31 // Parse body 32 let body; 33 try { 34 body = await request.json(); 35 } catch { 36 return errorResponse("Invalid JSON body", 400); 37 } 38 39 // Validate required fields 40 if (!body.data) { 41 return errorResponse("Missing required field: data", 400); 42 } 43 44 // Process request 45 const result = await ctx.runMutation(internal.process.handle, { 46 data: body.data, 47 }); 48 49 return jsonResponse({ success: true, result }, 200); 50 } catch (error) { 51 console.error("Processing error:", error); 52 return errorResponse("Internal server error", 500); 53 } 54 }), 55}); 56 57export default http;

File Downloads

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4import { Id } from "./_generated/dataModel"; 5 6const http = httpRouter(); 7 8http.route({ 9 pathPrefix: "/files/", 10 method: "GET", 11 handler: httpAction(async (ctx, request) => { 12 const url = new URL(request.url); 13 const fileId = url.pathname.replace("/files/", "") as Id<"_storage">; 14 15 // Get file URL from storage 16 const fileUrl = await ctx.storage.getUrl(fileId); 17 18 if (!fileUrl) { 19 return new Response("File not found", { status: 404 }); 20 } 21 22 // Redirect to the file URL 23 return Response.redirect(fileUrl, 302); 24 }), 25}); 26 27export default http;

Examples

Complete Webhook Integration

typescript
1// convex/http.ts 2import { httpRouter } from "convex/server"; 3import { httpAction } from "./_generated/server"; 4import { internal } from "./_generated/api"; 5 6const http = httpRouter(); 7 8// Clerk webhook for user sync 9http.route({ 10 path: "/webhooks/clerk", 11 method: "POST", 12 handler: httpAction(async (ctx, request) => { 13 const svixId = request.headers.get("svix-id"); 14 const svixTimestamp = request.headers.get("svix-timestamp"); 15 const svixSignature = request.headers.get("svix-signature"); 16 17 if (!svixId || !svixTimestamp || !svixSignature) { 18 return new Response("Missing Svix headers", { status: 400 }); 19 } 20 21 const body = await request.text(); 22 23 try { 24 await ctx.runAction(internal.clerk.verifyAndProcess, { 25 body, 26 svixId, 27 svixTimestamp, 28 svixSignature, 29 }); 30 return new Response("OK", { status: 200 }); 31 } catch (error) { 32 console.error("Clerk webhook error:", error); 33 return new Response("Webhook verification failed", { status: 400 }); 34 } 35 }), 36}); 37 38export default http;
typescript
1// convex/clerk.ts 2"use node"; 3 4import { internalAction, internalMutation } from "./_generated/server"; 5import { internal } from "./_generated/api"; 6import { v } from "convex/values"; 7import { Webhook } from "svix"; 8 9export const verifyAndProcess = internalAction({ 10 args: { 11 body: v.string(), 12 svixId: v.string(), 13 svixTimestamp: v.string(), 14 svixSignature: v.string(), 15 }, 16 returns: v.null(), 17 handler: async (ctx, args) => { 18 const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!; 19 const wh = new Webhook(webhookSecret); 20 21 const event = wh.verify(args.body, { 22 "svix-id": args.svixId, 23 "svix-timestamp": args.svixTimestamp, 24 "svix-signature": args.svixSignature, 25 }) as { type: string; data: Record<string, unknown> }; 26 27 switch (event.type) { 28 case "user.created": 29 await ctx.runMutation(internal.users.create, { 30 clerkId: event.data.id as string, 31 email: (event.data.email_addresses as Array<{ email_address: string }>)[0]?.email_address, 32 name: `${event.data.first_name} ${event.data.last_name}`, 33 }); 34 break; 35 36 case "user.updated": 37 await ctx.runMutation(internal.users.update, { 38 clerkId: event.data.id as string, 39 email: (event.data.email_addresses as Array<{ email_address: string }>)[0]?.email_address, 40 name: `${event.data.first_name} ${event.data.last_name}`, 41 }); 42 break; 43 44 case "user.deleted": 45 await ctx.runMutation(internal.users.remove, { 46 clerkId: event.data.id as string, 47 }); 48 break; 49 } 50 51 return null; 52 }, 53});

Schema for HTTP API

typescript
1// convex/schema.ts 2import { defineSchema, defineTable } from "convex/server"; 3import { v } from "convex/values"; 4 5export default defineSchema({ 6 apiKeys: defineTable({ 7 key: v.string(), 8 userId: v.id("users"), 9 name: v.string(), 10 createdAt: v.number(), 11 lastUsedAt: v.optional(v.number()), 12 revokedAt: v.optional(v.number()), 13 }) 14 .index("by_key", ["key"]) 15 .index("by_user", ["userId"]), 16 17 webhookEvents: defineTable({ 18 source: v.string(), 19 eventType: v.string(), 20 payload: v.any(), 21 processedAt: v.number(), 22 status: v.union( 23 v.literal("success"), 24 v.literal("failed") 25 ), 26 error: v.optional(v.string()), 27 }) 28 .index("by_source", ["source"]) 29 .index("by_status", ["status"]), 30 31 users: defineTable({ 32 clerkId: v.string(), 33 email: v.string(), 34 name: v.string(), 35 }).index("by_clerk_id", ["clerkId"]), 36});

Best Practices

  • Never run npx convex deploy unless explicitly instructed
  • Never run any git commands unless explicitly instructed
  • Always validate and sanitize incoming request data
  • Use internal functions for database operations
  • Implement proper error handling with appropriate status codes
  • Add CORS headers for browser-accessible endpoints
  • Verify webhook signatures before processing
  • Log webhook events for debugging
  • Use environment variables for secrets
  • Handle timeouts gracefully

Common Pitfalls

  1. Missing CORS preflight handler - Browsers send OPTIONS requests first
  2. Not validating webhook signatures - Security vulnerability
  3. Exposing internal functions - Use internal functions from HTTP actions
  4. Forgetting Content-Type headers - Clients may not parse responses correctly
  5. Not handling request body errors - Invalid JSON will throw
  6. Blocking on long operations - Use scheduled functions for heavy processing

References

Related Skills

Looking for an alternative to convex-http-actions or building a Categories.community AI Agent? Explore these related open-source MCP Servers.

View All

widget-generator

Logo of f
f

widget-generator is an open-source AI agent skill for creating widget plugins that are injected into prompt feeds on prompts.chat. It supports two rendering modes: standard prompt widgets using default PromptCard styling and custom render widgets built as full React components.

149.6k
0
Design

chat-sdk

Logo of lobehub
lobehub

chat-sdk is a unified TypeScript SDK for building chat bots across multiple platforms, providing a single interface for deploying bot logic.

73.0k
0
Communication

zustand

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication

data-fetching

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication