convex-http-actions
External API integration and webhook handling including HTTP endpoint routing, request/response handling, authentication, CORS configuration, and webhook signature validation
By waynesutton · 2,610 installs
npx skills add waynesutton/convexskills --skill convex-http-actions
Source repository · Upstream listing
Convex HTTP actions
Defines public HTTP endpoints served from the deployment's .convex.site domain. The one rule: an httpAction handler treats every byte of the request as untrusted, verifies it, then calls internal. functions with ctx.runQuery or ctx.runMutation . It never reads or writes ctx.db directly.
When to reach for this
A third party (Stripe, Clerk, Resend, GitHub) needs a URL to POST events to
A client without the Convex SDK (mobile app, CLI, another backend) needs a plain HTTP API
A browser needs to download or stream bytes without going through useQuery
A script or form needs to upload bytes over HTTP instead of an upload URL
httpAction or mutation
Caller Use
Your own React or Next.js app using convex/react query and mutation . Skip HTTP actions.
External service sending webhooks httpAction
Client that cannot use the Convex SDK httpAction
Scheduled or internal work internalMutation or internalAction
HTTP actions get no argument validators and no automatic auth. They run in the default Convex runtime, so Web APIs ( fetch , crypto.subtle , Response , ReadableStream ) work without "use node" . Request and response bodies are capped at 20MB.
Router skeleton
The router must live at convex/http.ts and be the default export. A router in any other file is ignored.
The endpoint is reachable at https://<deployment .convex.site/api/items . Note .convex.site , not .convex.cloud . path matches exactly; use pathPrefix: "/api/items/" for dynamic segments and read the tail from new URL(request.url).pathname .
Calling queries and mutations from the handler
ctx.runQuery , ctx.runMutation , ctx.runAction , ctx.storage , ctx.scheduler , and ctx.auth are all available inside httpAction . Point them at internal. functions so database logic is not also exposed as a public Convex function. Arguments still pass through the target's validators, so a bad shape fails there with a clear error.
For slow work (sending email, calling an LLM), respond 200 right away and hand off with ctx.scheduler.runAfter(0, internal.jobs.process, args) . Providers time out and retry if the handler stalls.
Webhook with signature verification
Read the raw body once with request.text() . Parsing first changes the bytes and breaks the signature. This generic HMAC SHA 256 pattern covers most providers; Stripe, Clerk, and Resend specifics are in the reference below.
Providers retry on any non 2xx and sometimes on network flakes, so the same event arrives more than once. Deduplicate by event id inside the mutation, which is a transaction:
Schema: webhookEvents: defineTable({ source: v.string(), eventId: v.string(), type: v.string(), payload: v.any() }).index("by source and event id", ["source", "eventId"]) .
Open [references/webhooks.md](references/webhooks.md) for Stripe ( constructEvent in a Node action), Clerk and Resend (svix headers), replay protection with timestamps, and a fuller idempotency table with status tracking.
CORS, path params, auth headers, streaming, files
Browsers send an OPTIONS preflight before cross origin POST s, so each browser facing path needs an OPTIONS route returning the same Access Control headers as the real route. Bearer tokens from your configured auth provider are read with await ctx.auth.getUserIdentity() , the same call used in queries. Streaming works by returning new Response(readableStream) , and files are served with ctx.storage.get(storageId) or accepted with request.blob() then ctx.storage.store(blob) .
Open [references/rest and cors.md](references/rest and cors.md) when building a REST style API: path parameters and id validation, the CORS preflight pair, JSON error conventions, bearer and API key auth, streaming responses, and file upload and download routes.
Common mistakes
Mistake Why it breaks Do instead
Router in convex/api.ts or convex/routes.ts Only convex/http.ts is loaded as the router Keep one http.ts with export default http
Calling https://<deployment .convex.cloud/webhooks/... HTTP actions live on .convex.site Use the .convex.site URL in the provider dashboard
await request.json() before verifying the signature The reserialized body no longer matches the signed bytes request.text() once, verify, then JSON.parse
Trusting the body because "it came from Stripe" Anyone can POST to a public URL Verify the signature on every request
Using api. targets from runMutation Exposes the same logic twice, once unauthenticated Target internal. functions
No OPTIONS route for a browser called endpoint Preflight fails, the real request never sends Add an OPTIONS route per path with CORS headers
Doing minutes of work inside the handler Provider times out and retries, duplicating work Return 200 fast, schedule with ctx.scheduler.runAfter
Processing the same event twice Providers retry on any non 2xx Dedupe by event id in the mutation
Missing Content Type: application/json on responses Clients get text they cannot parse Use a json() helper for every JSON response
Checklist
[ ] Router is at convex/http.ts and exported as default
[ ] Every route parses the body inside try/catch and returns 400 on bad input
[ ] Handlers call internal. functions, never ctx.db
[ ] Webhook routes read request.text() once and verify the signature before parsing
[ ] Webhook secrets come from process.env and are set in the deployment, not hardcoded
[ ] Events are deduplicated by provider event id inside a mutation
[ ] Slow work is scheduled, and the handler returns 2xx quickly
[ ] Browser called routes have a matching OPTIONS route
[ ] The provider dashboard points at the .convex.site URL
[ ] JSON responses set Content Type: application/json
Docs
https://docs.convex.dev/llms.txt
https://docs.convex.dev/functions/http actions
https://docs.convex.dev/auth/clerk
https://docs.convex.dev/file storage/serve files