openapi: 3.1.0
info:
  title: Emithook API
  version: "0.1.0"
  description: >
    The Emithook management + Send API. One scoped API powers the console,
    CLI, SDKs and MCP server. All requests are authenticated with a scoped
    API key (`Authorization: Bearer ek_live_…`).


    **Pre-release:** Emithook is `v0.1` and surfaces ship by roadmap phase.
    Paths not yet implemented return `501 not_implemented`. See the docs
    Availability legend. Conventions shared by every path (IDs, errors,
    pagination, idempotency, scopes, rate limits) are documented at
    `/docs/reference/conventions`.
servers:
  - url: https://api.emithook.com
security:
  - apiKey: []
tags:
  - name: Send
    description: Deliver outbound webhooks (Phase 2). Direct send and per-application fan-out.
  - name: Destinations
    description: The central outbound destination registry (Phase 1/2).
  - name: Endpoints
    description: Inbound entries you hand to providers (Phase 1).
  - name: Events
    description: Query delivered events and attempts; replay (Phase 1).
  - name: DLQ
    description: Dead-letter queue operations (Phase 1).
  - name: Metrics
    description: Pre-aggregated per-endpoint / per-destination rollups (Phase 1).
  - name: Domains
    description: Custom domain / email-domain verification (Phase 2).
  - name: Pull
    description: Cursor-based pull delivery for firewalled consumers (Phase 3).
  - name: Inbox
    description: Inbound email (MX engine) — list and read received mail (Roadmap).
paths:
  /v1/send:
    post:
      tags: [Send]
      summary: Send a webhook
      operationId: sendWebhook
      description: >
        Deliver one payload to a registered destination (or a validated ad-hoc
        HTTPS URL). Signed (Standard Webhooks), retried, circuit-broken, logged.
        Send `Idempotency-Key` to make client retries safe.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destination, event_type, payload]
              properties:
                destination:
                  type: string
                  description: A registered destination id, or an HTTPS URL.
                  example: dst_01JX9
                event_type: { type: string, example: invoice.created }
                payload:
                  type: object
                  example: { id: "INV-2026-001", amount: 4999, currency: "INR" }
                headers:
                  type: object
                  additionalProperties: { type: string }
                  description: Optional extra headers to forward.
      responses:
        "202":
          description: Accepted — queued for delivery.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message_id: { type: string, example: msg_01JX9 }
        "401": { $ref: '#/components/responses/Unauthenticated' }
        "403": { $ref: '#/components/responses/InsufficientScope' }
        "422": { $ref: '#/components/responses/ValidationFailed' }
        "429": { $ref: '#/components/responses/RateLimited' }
  /v1/app/{app_id}/event:
    post:
      tags: [Send]
      summary: Emit an event to an application (fan-out)
      operationId: sendAppEvent
      description: >
        Fan one event out to every endpoint the application has subscribed to
        the event type. The application may be auto-created on first send.
      parameters:
        - name: app_id
          in: path
          required: true
          schema: { type: string }
          description: The application id, or your own end-customer id (uid).
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event_type, payload]
              properties:
                event_type: { type: string, example: invoice.created }
                payload: { type: object }
      responses:
        "202":
          description: Accepted — fanned out to matching endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message_id: { type: string, example: msg_01JX9 }
                  fanout: { type: integer, example: 3 }
        "401": { $ref: '#/components/responses/Unauthenticated' }
        "404": { $ref: '#/components/responses/NotFound' }
  /v1/destinations:
    get:
      tags: [Destinations]
      summary: List destinations
      operationId: listDestinations
      parameters:
        - { name: type, in: query, schema: { $ref: '#/components/schemas/DestinationType' } }
        - { name: validation, in: query, schema: { type: string, enum: [valid, stale, invalid] } }
        - $ref: '#/components/parameters/Cursor'
      responses:
        "200":
          description: A page of destinations.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DestinationPage' }
    post:
      tags: [Destinations]
      summary: Create a destination
      operationId: createDestination
      description: >
        Adds a registry entry. The entry is created `pending` and only becomes
        `active` once schema, credentials and a live connectivity test pass.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, type]
              properties:
                name: { type: string, example: acme-orders }
                type: { $ref: '#/components/schemas/DestinationType' }
                url: { type: string, example: "https://api.acme.in/hooks/orders" }
                config:
                  type: object
                  description: Adapter-specific connection config (for queue types).
      responses:
        "201":
          description: Created — pending validation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Destination' }
        "422": { $ref: '#/components/responses/ValidationFailed' }
  /v1/destinations/{id}/validate:
    post:
      tags: [Destinations]
      summary: Re-validate a destination
      operationId: validateDestination
      description: Re-run the schema, credential and connectivity checks.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Validation report.
          content:
            application/json:
              schema:
                type: object
                properties:
                  validation: { type: string, enum: [valid, stale, invalid] }
                  checks:
                    type: object
                    properties:
                      schema: { type: boolean }
                      credentials: { type: boolean }
                      connectivity: { type: boolean }
  /v1/endpoints:
    post:
      tags: [Endpoints]
      summary: Create an inbound endpoint
      operationId: createEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug]
              properties:
                slug: { type: string, example: shopify-store }
                path: { type: string, example: /orders }
                preset:
                  type: string
                  description: Provider preset (signature scheme + sync response + retry defaults).
                  example: shopify
                destinations:
                  type: array
                  items: { type: string }
      responses:
        "201":
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Endpoint' }
        "409": { $ref: '#/components/responses/Conflict' }
  /v1/endpoints/{id}/secret:
    put:
      tags: [Endpoints]
      summary: Set the inbound verification secret
      operationId: setEndpointSecret
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [secret]
              properties:
                secret: { type: string, example: "shpss_…" }
      responses:
        "204": { description: Stored. }
  /v1/events:
    get:
      tags: [Events]
      summary: List events
      operationId: listEvents
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [delivered, failed, retrying, dlq] } }
        - { name: endpoint, in: query, schema: { type: string } }
        - { name: event_type, in: query, schema: { type: string } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { name: until, in: query, schema: { type: string, format: date-time } }
        - $ref: '#/components/parameters/Cursor'
      responses:
        "200":
          description: A page of events.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventPage' }
  /v1/events/{id}:
    get:
      tags: [Events]
      summary: Get an event with its attempts
      operationId: getEvent
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: The event and every delivery attempt.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Event' }
        "404": { $ref: '#/components/responses/NotFound' }
  /v1/events/{id}/replay:
    post:
      tags: [Events]
      summary: Replay an event
      operationId: replayEvent
      description: "Re-deliver a single event. The replay is flagged `webhook-replayed: true`."
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "202": { description: Replay queued. }
        "404": { $ref: '#/components/responses/NotFound' }
  /v1/dlq/redrive:
    post:
      tags: [DLQ]
      summary: Redrive dead-lettered events
      operationId: redriveDlq
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                endpoint: { type: string }
                since: { type: string, format: date-time }
      responses:
        "202":
          description: Redrive started.
          content:
            application/json:
              schema:
                type: object
                properties:
                  redriven: { type: integer, example: 42 }
  /v1/metrics:
    get:
      tags: [Metrics]
      summary: Read pre-aggregated metrics (rollups)
      operationId: getMetrics
      description: >
        Per-window rollups for one endpoint or destination, refreshed by a
        scheduled job (not aggregated on read).
      parameters:
        - name: key
          in: query
          required: true
          schema: { type: string }
          description: An endpoint or destination id you own.
      responses:
        "200":
          description: The key's rollups across windows.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Rollup' }
        "404": { $ref: '#/components/responses/NotFound' }
  /v1/domains:
    get:
      tags: [Domains]
      summary: List custom domains
      operationId: listDomains
      responses:
        "200":
          description: Domains with verification + TLS state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        domain: { type: string, example: acme.in }
                        state: { type: string, enum: [pending-dns, verifying, active, failed] }
                        records:
                          type: array
                          items: { type: object }
    post:
      tags: [Domains]
      summary: Add a custom domain (self-service)
      operationId: addDomain
      description: >
        Register a domain and receive the DNS records to publish. Created
        `pending-dns`; call verify once the records are live.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string, example: acme.in }
                kind: { type: string, enum: [webhook, email], example: webhook }
      responses:
        "201":
          description: Created — publish the returned DNS records.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, example: dom_01JX9 }
                  domain: { type: string }
                  state: { type: string, enum: [pending-dns, verifying, active, failed] }
                  records:
                    type: array
                    items: { type: object }
        "409": { $ref: '#/components/responses/Conflict' }
  /v1/domains/{id}/verify:
    post:
      tags: [Domains]
      summary: Verify a custom domain's DNS
      operationId: verifyDomain
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: The domain's verification state after re-checking DNS.
          content:
            application/json:
              schema:
                type: object
                properties:
                  state: { type: string, enum: [pending-dns, verifying, active, failed] }
        "404": { $ref: '#/components/responses/NotFound' }
  /v1/pull/{endpoint}:
    get:
      tags: [Pull]
      summary: Pull events (cursor-based)
      operationId: pullEvents
      description: >
        For consumers behind a firewall. Cursor-based, batch up to 100; advancing
        the cursor (or an explicit ack) commits. Availability window = the domain's
        retention setting.
      parameters:
        - { name: endpoint, in: path, required: true, schema: { type: string } }
        - $ref: '#/components/parameters/Cursor'
        - { name: limit, in: query, schema: { type: integer, maximum: 100, default: 50 } }
      responses:
        "200":
          description: A batch of events.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventPage' }
  /v1/aliases/{alias}/messages:
    get:
      tags: [Inbox]
      summary: List received emails for an alias
      operationId: listAliasMessages
      description: Inbound email parsed by the MX engine, with parsed fields and attachment links.
      parameters:
        - { name: alias, in: path, required: true, schema: { type: string }, example: support }
        - $ref: '#/components/parameters/Cursor'
      responses:
        "200":
          description: A page of messages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        message_id: { type: string }
                        from: { type: string, example: jane@buyer.com }
                        subject: { type: string }
                        received_at: { type: string, format: date-time }
                        auth: { type: string, enum: [verified, failed], description: SPF/DKIM/DMARC result }
                  next_cursor: { type: string, nullable: true }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: ek_live_…
      description: >
        Scoped API key. Scopes are `read` ⊂ `write` ⊂ `admin`. A call needing
        more scope than the key holds returns 403 insufficient_scope.
  parameters:
    Cursor:
      name: cursor
      in: query
      required: false
      schema: { type: string }
      description: Opaque pagination cursor; pass back `next_cursor` from the previous page.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, format: uuid }
      description: >
        POST only. Duplicate requests with the same key within ~12–24 h return
        the original response and create no second effect.
  schemas:
    DestinationType:
      type: string
      enum: [https, sqs, sns, pubsub, azure, amqp, kafka, nats, redis, pull]
    Destination:
      type: object
      properties:
        id: { type: string, example: dst_01JX9 }
        name: { type: string, example: acme-orders }
        type: { $ref: '#/components/schemas/DestinationType' }
        validation: { type: string, enum: [valid, stale, invalid, pending], example: pending }
        version: { type: integer, example: 1 }
    DestinationPage:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Destination' }
        next_cursor: { type: string, nullable: true }
    Endpoint:
      type: object
      properties:
        id: { type: string, example: ep_01JX9 }
        url: { type: string, example: "https://in.emithook.com/shopify-store/orders" }
        verification: { type: string, example: hmac-sha256 }
        status: { type: string, enum: [active, paused], example: active }
    Attempt:
      type: object
      properties:
        number: { type: integer, example: 1 }
        status: { type: integer, example: 200 }
        duration_ms: { type: integer, example: 142 }
        at: { type: string, format: date-time }
        response_excerpt: { type: string }
    Event:
      type: object
      properties:
        id: { type: string, example: evt_01JX9 }
        event_type: { type: string, example: orders/create }
        endpoint: { type: string, example: /shopify-store/orders }
        status: { type: string, enum: [delivered, failed, retrying, dlq] }
        received_at: { type: string, format: date-time }
        attempts:
          type: array
          items: { $ref: '#/components/schemas/Attempt' }
    EventPage:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Event' }
        next_cursor: { type: string, nullable: true }
    Rollup:
      type: object
      properties:
        key: { type: string, example: ep_01JX9 }
        window: { type: string, example: 1h }
        volume: { type: integer, example: 12043 }
        success_rate: { type: number, example: 0.998 }
        p50: { type: integer, nullable: true, example: 84 }
        p95: { type: integer, nullable: true, example: 240 }
        error_mix:
          type: object
          additionalProperties: { type: integer }
          nullable: true
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type: { type: string, example: validation_failed }
            message: { type: string }
            request_id: { type: string, example: req_01JX9 }
            details: { type: object }
  responses:
    Unauthenticated:
      description: Missing/invalid/expired key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    InsufficientScope:
      description: The key lacks the required scope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: No such resource.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: Duplicate (e.g. slug) or idempotency-key reuse with a different body.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationFailed:
      description: Schema/credential/connectivity check failed (see details.check).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Too many requests; honor Retry-After.
      headers:
        Retry-After: { schema: { type: integer }, description: Seconds to wait. }
        RateLimit-Remaining: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
