{"id":"convex-http-actions","name":"convex-http-actions","summary":"外部API統合およびウェブフックの処理(HTTPエンドポイントルーティング、要求/レスポンス処理、認証、CORS設定、ウェブフック署名の検証などが含まれます)","body":"# Convex HTTP Actions\n\nBuild HTTP endpoints for webhooks, external API integrations, and custom routes in Convex applications.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https://docs.convex.dev/functions/http-actions\n- Actions Overview: https://docs.convex.dev/functions/actions\n- Authentication: https://docs.convex.dev/auth\n- For broader context: https://docs.convex.dev/llms.txt\n\n## Instructions\n\n### HTTP Actions Overview\n\nHTTP actions allow you to define HTTP endpoints in Convex that can:\n\n- Receive webhooks from third-party services\n- Create custom API routes\n- Handle file uploads\n- Integrate with external services\n- Serve dynamic content\n\n### Basic HTTP Router Setup\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\n\nconst http = httpRouter();\n\n// Simple GET endpoint\nhttp.route({\n  path: \"/health\",\n  method: \"GET\",\n  handler: httpAction(async (ctx, request) => {\n    return new Response(JSON.stringify({ status: \"ok\" }), {\n      status: 200,\n      headers: { \"Content-Type\": \"application/json\" },\n    });\n  }),\n});\n\nexport default http;\n```\n\n### Request Handling\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\n\nconst http = httpRouter();\n\n// Handle JSON body\nhttp.route({\n  path: \"/api/data\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    // Parse JSON body\n    const body = await request.json();\n    \n    // Access headers\n    const authHeader = request.headers.get(\"Authorization\");\n    \n    // Access URL parameters\n    const url = new URL(request.url);\n    const queryParam = url.searchParams.get(\"filter\");\n\n    return new Response(\n      JSON.stringify({ received: body, filter: queryParam }),\n      {\n        status: 200,\n        headers: { \"Content-Type\": \"application/json\" },\n      }\n    );\n  }),\n});\n\n// Handle form data\nhttp.route({\n  path: \"/api/form\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const formData = await request.formData();\n    const name = formData.get(\"name\");\n    const email = formData.get(\"email\");\n\n    return new Response(\n      JSON.stringify({ name, email }),\n      {\n        status: 200,\n        headers: { \"Content-Type\": \"application/json\" },\n      }\n    );\n  }),\n});\n\n// Handle raw bytes\nhttp.route({\n  path: \"/api/upload\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const bytes = await request.bytes();\n    const contentType = request.headers.get(\"Content-Type\") ?? \"application/octet-stream\";\n    \n    // Store in Convex storage\n    const blob = new Blob([bytes], { type: contentType });\n    const storageId = await ctx.storage.store(blob);\n\n    return new Response(\n      JSON.stringify({ storageId }),\n      {\n        status: 200,\n        headers: { \"Content-Type\": \"application/json\" },\n      }\n    );\n  }),\n});\n\nexport default http;\n```\n\n### Path Parameters\n\nUse path prefix matching for dynamic routes:\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\n\nconst http = httpRouter();\n\n// Match /api/users/* with pathPrefix\nhttp.route({\n  pathPrefix: \"/api/users/\",\n  method: \"GET\",\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url);\n    // Extract user ID from path: /api/users/123 -> \"123\"\n    const userId = url.pathname.replace(\"/api/users/\", \"\");\n\n    return new Response(\n      JSON.stringify({ userId }),\n      {\n        status: 200,\n        headers: { \"Content-Type\": \"application/json\" },\n      }\n    );\n  }),\n});\n\nexport default http;\n```\n\n### CORS Configuration\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\n\nconst http = httpRouter();\n\n// CORS headers helper\nconst corsHeaders = {\n  \"Access-Control-Allow-Origin\": \"*\",\n  \"Access-Control-Allow-Methods\": \"GET, POST, PUT, DELETE, OPTIONS\",\n  \"Access-Control-Allow-Headers\": \"Content-Type, Authorization\",\n  \"Access-Control-Max-Age\": \"86400\",\n};\n\n// Handle preflight requests\nhttp.route({\n  path: \"/api/data\",\n  method: \"OPTIONS\",\n  handler: httpAction(async () => {\n    return new Response(null, {\n      status: 204,\n      headers: corsHeaders,\n    });\n  }),\n});\n\n// Actual endpoint with CORS\nhttp.route({\n  path: \"/api/data\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const body = await request.json();\n\n    return new Response(\n      JSON.stringify({ success: true, data: body }),\n      {\n        status: 200,\n        headers: {\n          \"Content-Type\": \"application/json\",\n          ...corsHeaders,\n        },\n      }\n    );\n  }),\n});\n\nexport default http;\n```\n\n### Webhook Handling\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\nimport { internal } from \"./_generated/api\";\n\nconst http = httpRouter();\n\n// Stripe webhook\nhttp.route({\n  path: \"/webhooks/stripe\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const signature = request.headers.get(\"stripe-signature\");\n    if (!signature) {\n      return new Response(\"Missing signature\", { status: 400 });\n    }\n\n    const body = await request.text();\n\n    // Verify webhook signature (in action with Node.js)\n    try {\n      await ctx.runAction(internal.stripe.verifyAndProcessWebhook, {\n        body,\n        signature,\n      });\n      return new Response(\"OK\", { status: 200 });\n    } catch (error) {\n      console.error(\"Webhook error:\", error);\n      return new Response(\"Webhook error\", { status: 400 });\n    }\n  }),\n});\n\n// GitHub webhook\nhttp.route({\n  path: \"/webhooks/github\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const event = request.headers.get(\"X-GitHub-Event\");\n    const signature = request.headers.get(\"X-Hub-Signature-256\");\n    \n    if (!signature) {\n      return new Response(\"Missing signature\", { status: 400 });\n    }\n\n    const body = await request.text();\n\n    await ctx.runAction(internal.github.processWebhook, {\n      event: event ?? \"unknown\",\n      body,\n      signature,\n    });\n\n    return new Response(\"OK\", { status: 200 });\n  }),\n});\n\nexport default http;\n```\n\n### Webhook Signature Verification\n\n```typescript\n// convex/stripe.ts\n\"use node\";\n\nimport { internalAction, internalMutation } from \"./_generated/server\";\nimport { internal } from \"./_generated/api\";\nimport { v } from \"convex/values\";\nimport Stripe from \"stripe\";\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\nexport const verifyAndProcessWebhook = internalAction({\n  args: {\n    body: v.string(),\n    signature: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;\n\n    // Verify signature\n    const event = stripe.webhooks.constructEvent(\n      args.body,\n      args.signature,\n      webhookSecret\n    );\n\n    // Process based on event type\n    switch (event.type) {\n      case \"checkout.session.completed\":\n        await ctx.runMutation(internal.payments.handleCheckoutComplete, {\n          sessionId: event.data.object.id,\n          customerId: event.data.object.customer as string,\n        });\n        break;\n\n      case \"customer.subscription.updated\":\n        await ctx.runMutation(internal.subscriptions.handleUpdate, {\n          subscriptionId: event.data.object.id,\n          status: event.data.object.status,\n        });\n        break;\n    }\n\n    return null;\n  },\n});\n```\n\n### Authentication in HTTP Actions\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\nimport { internal } from \"./_generated/api\";\n\nconst http = httpRouter();\n\n// API key authentication\nhttp.route({\n  path: \"/api/protected\",\n  method: \"GET\",\n  handler: httpAction(async (ctx, request) => {\n    const apiKey = request.headers.get(\"X-API-Key\");\n    \n    if (!apiKey) {\n      return new Response(\n        JSON.stringify({ error: \"Missing API key\" }),\n        { status: 401, headers: { \"Content-Type\": \"application/json\" } }\n      );\n    }\n\n    // Validate API key\n    const isValid = await ctx.runQuery(internal.auth.validateApiKey, {\n      apiKey,\n    });\n\n    if (!isValid) {\n      return new Response(\n        JSON.stringify({ error: \"Invalid API key\" }),\n        { status: 403, headers: { \"Content-Type\": \"application/json\" } }\n      );\n    }\n\n    // Process authenticated request\n    const data = await ctx.runQuery(internal.data.getProtectedData, {});\n\n    return new Response(\n      JSON.stringify(data),\n      { status: 200, headers: { \"Content-Type\": \"application/json\" } }\n    );\n  }),\n});\n\n// Bearer token authentication\nhttp.route({\n  path: \"/api/user\",\n  method: \"GET\",\n  handler: httpAction(async (ctx, request) => {\n    const authHeader = request.headers.get(\"Authorization\");\n    \n    if (!authHeader?.startsWith(\"Bearer \")) {\n      return new Response(\n        JSON.stringify({ error: \"Missing or invalid Authorization header\" }),\n        { status: 401, headers: { \"Content-Type\": \"application/json\" } }\n      );\n    }\n\n    const token = authHeader.slice(7);\n\n    // Validate token and get user\n    const user = await ctx.runQuery(internal.auth.validateToken, { token });\n\n    if (!user) {\n      return new Response(\n        JSON.stringify({ error: \"Invalid token\" }),\n        { status: 403, headers: { \"Content-Type\": \"application/json\" } }\n      );\n    }\n\n    return new Response(\n      JSON.stringify(user),\n      { status: 200, headers: { \"Content-Type\": \"application/json\" } }\n    );\n  }),\n});\n\nexport default http;\n```\n\n### Calling Mutations and Queries\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\nimport { api, internal } from \"./_generated/api\";\n\nconst http = httpRouter();\n\nhttp.route({\n  path: \"/api/items\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const body = await request.json();\n\n    // Call a mutation\n    const itemId = await ctx.runMutation(internal.items.create, {\n      name: body.name,\n      description: body.description,\n    });\n\n    // Query the created item\n    const item = await ctx.runQuery(internal.items.get, { id: itemId });\n\n    return new Response(\n      JSON.stringify(item),\n      { status: 201, headers: { \"Content-Type\": \"application/json\" } }\n    );\n  }),\n});\n\nhttp.route({\n  path: \"/api/items\",\n  method: \"GET\",\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url);\n    const limit = parseInt(url.searchParams.get(\"limit\") ?? \"10\");\n\n    const items = await ctx.runQuery(internal.items.list, { limit });\n\n    return new Response(\n      JSON.stringify(items),\n      { status: 200, headers: { \"Content-Type\": \"application/json\" } }\n    );\n  }),\n});\n\nexport default http;\n```\n\n### Error Handling\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\n\nconst http = httpRouter();\n\n// Helper for JSON responses\nfunction jsonResponse(data: unknown, status = 200) {\n  return new Response(JSON.stringify(data), {\n    status,\n    headers: { \"Content-Type\": \"application/json\" },\n  });\n}\n\n// Helper for error responses\nfunction errorResponse(message: string, status: number) {\n  return jsonResponse({ error: message }, status);\n}\n\nhttp.route({\n  path: \"/api/process\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    try {\n      // Validate content type\n      const contentType = request.headers.get(\"Content-Type\");\n      if (!contentType?.includes(\"application/json\")) {\n        return errorResponse(\"Content-Type must be application/json\", 415);\n      }\n\n      // Parse body\n      let body;\n      try {\n        body = await request.json();\n      } catch {\n        return errorResponse(\"Invalid JSON body\", 400);\n      }\n\n      // Validate required fields\n      if (!body.data) {\n        return errorResponse(\"Missing required field: data\", 400);\n      }\n\n      // Process request\n      const result = await ctx.runMutation(internal.process.handle, {\n        data: body.data,\n      });\n\n      return jsonResponse({ success: true, result }, 200);\n    } catch (error) {\n      console.error(\"Processing error:\", error);\n      return errorResponse(\"Internal server error\", 500);\n    }\n  }),\n});\n\nexport default http;\n```\n\n### File Downloads\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\nimport { Id } from \"./_generated/dataModel\";\n\nconst http = httpRouter();\n\nhttp.route({\n  pathPrefix: \"/files/\",\n  method: \"GET\",\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url);\n    const fileId = url.pathname.replace(\"/files/\", \"\") as Id<\"_storage\">;\n\n    // Get file URL from storage\n    const fileUrl = await ctx.storage.getUrl(fileId);\n\n    if (!fileUrl) {\n      return new Response(\"File not found\", { status: 404 });\n    }\n\n    // Redirect to the file URL\n    return Response.redirect(fileUrl, 302);\n  }),\n});\n\nexport default http;\n```\n\n## Examples\n\n### Complete Webhook Integration\n\n```typescript\n// convex/http.ts\nimport { httpRouter } from \"convex/server\";\nimport { httpAction } from \"./_generated/server\";\nimport { internal } from \"./_generated/api\";\n\nconst http = httpRouter();\n\n// Clerk webhook for user sync\nhttp.route({\n  path: \"/webhooks/clerk\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const svixId = request.headers.get(\"svix-id\");\n    const svixTimestamp = request.headers.get(\"svix-timestamp\");\n    const svixSignature = request.headers.get(\"svix-signature\");\n\n    if (!svixId || !svixTimestamp || !svixSignature) {\n      return new Response(\"Missing Svix headers\", { status: 400 });\n    }\n\n    const body = await request.text();\n\n    try {\n      await ctx.runAction(internal.clerk.verifyAndProcess, {\n        body,\n        svixId,\n        svixTimestamp,\n        svixSignature,\n      });\n      return new Response(\"OK\", { status: 200 });\n    } catch (error) {\n      console.error(\"Clerk webhook error:\", error);\n      return new Response(\"Webhook verification failed\", { status: 400 });\n    }\n  }),\n});\n\nexport default http;\n```\n\n```typescript\n// convex/clerk.ts\n\"use node\";\n\nimport { internalAction, internalMutation } from \"./_generated/server\";\nimport { internal } from \"./_generated/api\";\nimport { v } from \"convex/values\";\nimport { Webhook } from \"svix\";\n\nexport const verifyAndProcess = internalAction({\n  args: {\n    body: v.string(),\n    svixId: v.string(),\n    svixTimestamp: v.string(),\n    svixSignature: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!;\n    const wh = new Webhook(webhookSecret);\n\n    const event = wh.verify(args.body, {\n      \"svix-id\": args.svixId,\n      \"svix-timestamp\": args.svixTimestamp,\n      \"svix-signature\": args.svixSignature,\n    }) as { type: string; data: Record<string, unknown> };\n\n    switch (event.type) {\n      case \"user.created\":\n        await ctx.runMutation(internal.users.create, {\n          clerkId: event.data.id as string,\n          email: (event.data.email_addresses as Array<{ email_address: string }>)[0]?.email_address,\n          name: `${event.data.first_name} ${event.data.last_name}`,\n        });\n        break;\n\n      case \"user.updated\":\n        await ctx.runMutation(internal.users.update, {\n          clerkId: event.data.id as string,\n          email: (event.data.email_addresses as Array<{ email_address: string }>)[0]?.email_address,\n          name: `${event.data.first_name} ${event.data.last_name}`,\n        });\n        break;\n\n      case \"user.deleted\":\n        await ctx.runMutation(internal.users.remove, {\n          clerkId: event.data.id as string,\n        });\n        break;\n    }\n\n    return null;\n  },\n});\n```\n\n### Schema for HTTP API\n\n```typescript\n// convex/schema.ts\nimport { defineSchema, defineTable } from \"convex/server\";\nimport { v } from \"convex/values\";\n\nexport default defineSchema({\n  apiKeys: defineTable({\n    key: v.string(),\n    userId: v.id(\"users\"),\n    name: v.string(),\n    createdAt: v.number(),\n    lastUsedAt: v.optional(v.number()),\n    revokedAt: v.optional(v.number()),\n  })\n    .index(\"by_key\", [\"key\"])\n    .index(\"by_user\", [\"userId\"]),\n\n  webhookEvents: defineTable({\n    source: v.string(),\n    eventType: v.string(),\n    payload: v.any(),\n    processedAt: v.number(),\n    status: v.union(\n      v.literal(\"success\"),\n      v.literal(\"failed\")\n    ),\n    error: v.optional(v.string()),\n  })\n    .index(\"by_source\", [\"source\"])\n    .index(\"by_status\", [\"status\"]),\n\n  users: defineTable({\n    clerkId: v.string(),\n    email: v.string(),\n    name: v.string(),\n  }).index(\"by_clerk_id\", [\"clerkId\"]),\n});\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Always validate and sanitize incoming request data\n- Use internal functions for database operations\n- Implement proper error handling with appropriate status codes\n- Add CORS headers for browser-accessible endpoints\n- Verify webhook signatures before processing\n- Log webhook events for debugging\n- Use environment variables for secrets\n- Handle timeouts gracefully\n\n## Common Pitfalls\n\n1. **Missing CORS preflight handler** - Browsers send OPTIONS requests first\n2. **Not validating webhook signatures** - Security vulnerability\n3. **Exposing internal functions** - Use internal functions from HTTP actions\n4. **Forgetting Content-Type headers** - Clients may not parse responses correctly\n5. **Not handling request body errors** - Invalid JSON will throw\n6. **Blocking on long operations** - Use scheduled functions for heavy processing\n\n## References\n\n- Convex Documentation: https://docs.convex.dev/\n- Convex LLMs.txt: https://docs.convex.dev/llms.txt\n- HTTP Actions: https://docs.convex.dev/functions/http-actions\n- Actions: https://docs.convex.dev/functions/actions\n- Authentication: https://docs.convex.dev/auth","author":"@waynesutton","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/waynesutton/convexskills/tree/main/skills/convex-http-actions","license":"Apache-2.0","category":"document","lang":"en","tokens":4277,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"agents/openai.yaml","size":91,"sha256":"bb57e6929f0916464111ae4a5e0a2ec5d301653b5a3b818a81dc2c0a7deb21c2"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["docs.convex.dev"]}}