Skip to main content
HTTP triggers allow you to expose workflows as HTTP endpoints, enabling you to build REST APIs, webhooks, and HTTP-based integrations.

Basic usage

import { step, http } from 'motia'
import { z } from 'zod'

export const config = step({
  name: 'get-user',
  triggers: [http('GET', '/users/:id')],
})

export const handler = async (input, ctx) => {
  const userId = input.request.pathParams.id
  
  return {
    status: 200,
    body: { id: userId, name: 'John Doe' },
  }
}

HTTP methods

HTTP triggers support all standard HTTP methods:
import { step, http } from 'motia'

export const config = step({
  name: 'api-endpoints',
  triggers: [
    http('GET', '/items'),
    http('POST', '/items'),
    http('PUT', '/items/:id'),
    http('DELETE', '/items/:id'),
    http('PATCH', '/items/:id'),
  ],
})

Request handling

Path parameters

Extract dynamic values from the URL path:
import { step, http } from 'motia'

export const config = step({
  name: 'get-item',
  triggers: [http('GET', '/items/:id')],
})

export const handler = async (input, ctx) => {
  const { id } = input.request.pathParams
  
  return {
    status: 200,
    body: { id, name: 'Item' },
  }
}

Query parameters

Access query string parameters:
import { step, http } from 'motia'

export const config = step({
  name: 'search-items',
  triggers: [
    http('GET', '/search', {
      queryParams: [
        { name: 'q', description: 'Search query' },
        { name: 'limit', description: 'Results limit' },
      ],
    }),
  ],
})

export const handler = async (input, ctx) => {
  const { q, limit } = input.request.queryParams
  
  return {
    status: 200,
    body: { query: q, limit: limit || '10' },
  }
}

Request body

Handle POST/PUT/PATCH request bodies with schema validation:
import { step, http } from 'motia'
import { z } from 'zod'

const createItemSchema = z.object({
  name: z.string(),
  price: z.number(),
  description: z.string().optional(),
})

export const config = step({
  name: 'create-item',
  triggers: [
    http('POST', '/items', {
      bodySchema: createItemSchema,
    }),
  ],
})

export const handler = async (input, ctx) => {
  const { name, price, description } = input.request.body
  
  return {
    status: 201,
    body: {
      id: 'item-123',
      name,
      price,
      description,
      created: true,
    },
  }
}

Headers

Access request headers:
export const handler = async (input, ctx) => {
  const authHeader = input.request.headers['authorization']
  const contentType = input.request.headers['content-type']
  
  return {
    status: 200,
    body: { authenticated: !!authHeader },
  }
}

Response handling

Status codes

Return different HTTP status codes:
export const handler = async (input, ctx) => {
  const { id } = input.request.pathParams
  
  // Simulate item not found
  if (id === 'missing') {
    return {
      status: 404,
      body: { error: 'Item not found' },
    }
  }
  
  return {
    status: 200,
    body: { id, name: 'Found Item' },
  }
}

Response headers

Set custom response headers:
export const handler = async (input, ctx) => {
  return {
    status: 200,
    headers: {
      'X-Custom-Header': 'value',
      'Cache-Control': 'max-age=3600',
    },
    body: { message: 'Success' },
  }
}

Response schema

Define response schemas for type safety:
import { step, http } from 'motia'
import { z } from 'zod'

const successSchema = z.object({
  id: z.string(),
  name: z.string(),
})

const errorSchema = z.object({
  error: z.string(),
})

export const config = step({
  name: 'typed-response',
  triggers: [
    http('GET', '/items/:id', {
      responseSchema: {
        200: successSchema,
        404: errorSchema,
      },
    }),
  ],
})

Middleware

Add middleware functions for cross-cutting concerns:
import { step, http } from 'motia'
import type { ApiMiddleware } from 'motia'

const authMiddleware: ApiMiddleware = async (req, ctx, next) => {
  const token = req.request.headers['authorization']
  
  if (!token) {
    return {
      status: 401,
      body: { error: 'Unauthorized' },
    }
  }
  
  return await next()
}

const loggingMiddleware: ApiMiddleware = async (req, ctx, next) => {
  ctx.logger.info('Request received', {
    method: req.request.method,
    path: ctx.trigger.path,
  })
  
  return await next()
}

export const config = step({
  name: 'protected-endpoint',
  triggers: [
    http('GET', '/protected', {
      middleware: [loggingMiddleware, authMiddleware],
    }),
  ],
})

Conditional triggers

Use conditions to selectively execute handlers:
import { step, http } from 'motia'

export const config = step({
  name: 'conditional-api',
  triggers: [
    http(
      'POST',
      '/webhooks',
      undefined,
      (input, ctx) => {
        const signature = input.request.headers['x-webhook-signature']
        return !!signature
      },
    ),
  ],
})

Configuration options

method
string
required
HTTP method: GET, POST, PUT, DELETE, PATCH, OPTIONS, or HEAD
path
string
required
URL path pattern. Supports path parameters with :param syntax (e.g., /users/:id)
bodySchema
ZodSchema | JsonSchema
Schema for validating request body. Automatically validates incoming requests
responseSchema
Record<number, Schema>
Map of status codes to response schemas for type safety and documentation
queryParams
QueryParam[]
Array of query parameter definitions:
  • name (string): Parameter name
  • description (string): Parameter description
middleware
ApiMiddleware[]
Array of middleware functions executed before the handler. Each middleware can:
  • Modify the request
  • Return early with a response
  • Call next() to continue the chain
condition
function
Optional function (input, ctx) => boolean to conditionally execute the handler

Use cases

REST API

Build a complete REST API:
import { step, http } from 'motia'
import { z } from 'zod'

const itemSchema = z.object({
  name: z.string(),
  price: z.number(),
})

export const config = step({
  name: 'items-api',
  triggers: [
    http('GET', '/items'),
    http('GET', '/items/:id'),
    http('POST', '/items', { bodySchema: itemSchema }),
    http('PUT', '/items/:id', { bodySchema: itemSchema }),
    http('DELETE', '/items/:id'),
  ],
})

Webhook receiver

Receive webhooks from external services:
import { step, http } from 'motia'
import { z } from 'zod'

const webhookSchema = z.object({
  event: z.string(),
  data: z.record(z.unknown()),
})

export const config = step({
  name: 'github-webhook',
  triggers: [
    http('POST', '/webhooks/github', {
      bodySchema: webhookSchema,
    }),
  ],
})

export const handler = async (input, ctx) => {
  const { event, data } = input.request.body
  
  ctx.logger.info('Webhook received', { event })
  
  // Process webhook
  await ctx.enqueue({
    topic: 'webhook-events',
    data: { event, data },
  })
  
  return {
    status: 200,
    body: { received: true },
  }
}

File upload

Handle file uploads:
export const config = step({
  name: 'upload-file',
  triggers: [http('POST', '/upload')],
})

export const handler = async (input, ctx) => {
  const file = input.request.body
  
  // Process file
  ctx.logger.info('File uploaded', {
    size: file.length,
  })
  
  return {
    status: 201,
    body: { uploaded: true, fileId: 'file-123' },
  }
}

Build docs developers (and LLMs) love