Skip to main content

convex-http-actions

External API integration and webhook handling including HTTP endpoint routing, request/response handling, authentication, CORS configuration, and webhook signature validation

Jump to install

Source facts

Repository
mediar-ai/skillhubz
Last source activity
March 1, 2026 at 22:59
Detected SKILL.md language
English
Stars
7
Forks
4

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
convex-http-actions
displayName
Convex HTTP Actions
description
External API integration and webhook handling including HTTP endpoint routing, request/response handling, authentication, CORS configuration, and webhook signature validation
version
1.0.0
author
Convex
tags
["convex","http","actions","webhooks","api","endpoints"]
# 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: - Primary: https://docs.convex.dev/functions/http-actions - Actions Overview: https://docs.convex.dev/functions/actions - Authentication: https://docs.convex.dev/auth - For broader context: https://docs.convex.dev/llms.txt ## 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 // convex/http.ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; const http = httpRouter(); // Simple GET endpoint http.route({ path: "/health", method: "GET", handler: httpAction(async (ctx, request) => { return new Response(JSON.stringify({ status: "ok" }), { status: 200, headers: { "Content-Type": "application/json" }, }); }), }); export default http; ``` ### Request Handling ```typescript // convex/http.ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; const http = httpRouter(); // Handle JSON body http.route({ path: "/api/data", method: "POST", handler: httpAction(async (ctx, request) => { // Parse JSON body const body = await request.json(); // Access headers const authHeader = request.headers.get("Authorization"); // Access URL parameters const url = new URL(request.url); const queryParam = url.searchParams.get("filter"); return new Response( JSON.stringify({ received: body, filter: queryParam }), { status: 200, headers: { "Content-Type": "application/json" }, } ); }), }); // Handle form data http.route({ path: "/api/form", method: "POST", handler: httpAction(async (ctx, request) => { const formData = await request.formData(); const name = formData.get("name"); const email = formData.get("email"); return new Response( JSON.stringify({ name, email }), { status: 200, headers: { "Content-Type": "application/json" }, } ); }), }); // Handle raw bytes http.route({ path: "/api/upload", method: "POST", handler: httpAction(async (ctx, request) => { const bytes = await request.bytes(); const contentType = request.headers.get("Content-Type") ?? "application/octet-stream"; // Store in Convex storage const blob = new Blob([bytes], { type: contentType }); const storageId = await ctx.storage.store(blob); return new Response( JSON.stringify({ storageId }), { status: 200, headers: { "Content-Type": "application/json" }, } ); }), }); export default http; ``` ### Path Parameters Use path prefix matching for dynamic routes: ```typescript // convex/http.ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; const http = httpRouter(); // Match /api/users/* with pathPrefix http.route({ pathPrefix: "/api/users/", method: "GET", handler: httpAction(async (ctx, request) => { const url = new URL(request.url); // Extract user ID from path: /api/users/123 -> "123" const userId = url.pathname.replace("/api/users/", ""); return new Response( JSON.stringify({ userId }), { status: 200, headers: { "Content-Type": "application/json" }, } ); }), }); export default http; ``` ### CORS Configuration ```typescript // convex/http.ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; const http = httpRouter(); // CORS headers helper const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Max-Age": "86400", }; // Handle preflight requests http.route({ path: "/api/data", method: "OPTIONS", handler: httpAction(async () => { return new Response(null, { status: 204, headers: corsHeaders, }); }), }); // Actual endpoint with CORS http.route({ path: "/api/data", method: "POST", handler: httpAction(async (ctx, request) => { const body = await request.json(); return new Response( JSON.stringify({ success: true, data: body }), { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders, }, } ); }), }); export default http; ``` ### Webhook Handling ```typescript // convex/http.ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; import { internal } from "./_generated/api"; const http = httpRouter(); // Stripe webhook http.route({ path: "/webhooks/stripe", method: "POST", handler: httpAction(async (ctx, request) => { const signature = request.headers.get("stripe-signature"); if (!signature) { return new Response("Missing signature", { status: 400 }); } const body = await request.text(); // Verify webhook signature (in action with Node.js) try { await ctx.runAction(internal.stripe.verifyAndProcessWebhook, { body, signature, }); return new Response("OK", { status: 200 }); } catch (error) { console.error("Webhook error:", error); return new Response("Webhook error", { status: 400 }); } }), }); // GitHub webhook http.route({ path: "/webhooks/github", method: "POST", handler: httpAction(async (ctx, request) => { const event = request.headers.get("X-GitHub-Event"); const signature = request.headers.get("X-Hub-Signature-256"); if (!signature) { return new Response("Missing signature", { status: 400 }); } const body = await request.text(); await ctx.runAction(internal.github.processWebhook, { event: event ?? "unknown", body, signature, }); return new Response("OK", { status: 200 }); }), }); export default http; ``` ### Webhook Signature Verification ```typescript // convex/stripe.ts "use node"; import { internalAction, internalMutation } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export const verifyAndProcessWebhook = internalAction({ args: { body: v.string(), signature: v.string(), }, returns: v.null(), handler: async (ctx, args) => { const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!; // Verify signature const event = stripe.webhooks.constructEvent( args.body, args.signature, webhookSecret ); // Process based on event type switch (event.type) { case "checkout.session.completed": await ctx.runMutation(internal.payments.handleCheckoutComplete, { sessionId: event.data.object.id, customerId: event.data.object.customer as string, }); break; case "customer.subscription.updated": await ctx.runMutation(internal.subscriptions.handleUpdate, { subscriptionId: event.data.object.id, status: event.data.object.status, }); break; } return null; }, }); ``` ### Authentication in HTTP Actions ```typescript // convex/http.ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; import { internal } from "./_generated/api"; const http = httpRouter(); // API key authentication http.route({ path: "/api/protected", method: "GET", handler: httpAction(async (ctx, request) => { const apiKey = request.headers.get("X-API-Key"); if (!apiKey) { return new Response( JSON.stringify({ error: "Missing API key" }), { status: 401, headers: { "Content-Type": "application/json" } } ); } // Validate API key const isValid = await ctx.runQuery(internal.auth.validateApiKey, { apiKey, }); if (!isValid) { return new Response( JSON.stringify({ error: "Invalid API key" }), { status: 403, headers: { "Content-Type": "application/json" } } ); } // Process authenticated request const data = await ctx.runQuery(internal.data.getProtectedData, {}); return new Response( JSON.stringify(data), { status: 200, headers: { "Content-Type": "application/json" } } ); }), }); // Bearer token authentication http.route({ path: "/api/user", method: "GET", handler: httpAction(async (ctx, request) => { const authHeader = request.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { return new Response( JSON.stringify({ error: "Missing or invalid Authorization header" }), { status: 401, headers: { "Content-Type": "application/json" } } ); } const token = authHeader.slice(7); // Validate token and get user const user = await ctx.runQuery(internal.auth.validateToken, { token }); if (!user) { return new Response( JSON.stringify({ error: "Invalid token" }), { status: 403, headers: { "Content-Type": "application/json" } } ); } return new Response( JSON.stringify(user), { status: 200, headers: { "Content-Type": "application/json" } } ); }), }); export default http; ``` ### Calling Mutations and Queries ```typescript // convex/http.ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; import { api, internal } from "./_generated/api"; const http = httpRouter(); http.route({ path: "/api/items", method: "POST", handler: httpAction(async (ctx, request) => { const body = await request.json(); // Call a mutation const itemId = await ctx.runMutation(internal.items.create, { name: body.name, description: body.description, }); // Query the created item
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub