openapi: 3.1.0
info:
  title: Outemit API
  version: 1.0.0
  description: |
    Outbound webhook infrastructure. Emit messages, manage applications and endpoints,
    retry deliveries, and replay messages.

    Auth: `Authorization: Bearer sk_test_…` or `sk_live_…`.
    Prefer the `Idempotency-Key` header on emit.
    Readable docs: https://outemit.dev/docs/api/reference
  contact:
    name: Outemit
    url: https://outemit.dev
servers:
  - url: https://outemit.dev
    description: Production app / API
  - url: https://staging.outemit.dev
    description: Staging
security:
  - bearerAuth: []
tags:
  - name: Messages
    description: Emit, fetch, and replay immutable events
  - name: Applications
    description: Customer tenants that receive webhooks
  - name: Endpoints
    description: HTTPS (or localhost in test) receiver URLs
  - name: Deliveries
    description: Per-endpoint attempt paths (retry, not replay)
  - name: Event types
    description: Org event catalog
  - name: API keys
    description: Create additional org API keys
paths:
  /api/v1/messages:
    get:
      tags: [Messages]
      summary: List recent messages
      description: Returns up to 50 recent messages for the API key org (livemode-scoped).
      responses:
        "200":
          description: Message list
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, example: list }
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Message"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Messages]
      summary: Emit a message
      description: |
        Persist an immutable message and fan out to subscribed endpoints.
        Retrying the HTTP call with the same Idempotency-Key returns the original result.
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string }
          description: Dedupes identical emit calls
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmitMessageRequest"
            examples:
              invoicePaid:
                summary: invoice.paid
                value:
                  app_id: app_01HXYZ
                  event_type: invoice.paid
                  payload: { id: inv_1, amount: 4900 }
      responses:
        "201":
          description: Message created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmitMessageResponse"
              example:
                id: evt_01HABC
                event_type: invoice.paid
                app_id: app_01HXYZ
                delivery_ids: [dlv_01HDEF]
                idempotent: false
        "200":
          description: Idempotent replay of a prior emit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EmitMessageResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/messages/{id}:
    get:
      tags: [Messages]
      summary: Get a message
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Message metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Message"
              example:
                id: evt_01HABC
                event_type: invoice.paid
                app_id: app_01HXYZ
                created_at: "2026-09-05T12:00:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/messages/{id}/replay:
    post:
      tags: [Messages]
      summary: Replay a message through the pipeline
      description: |
        Creates NEW deliveries for currently subscribed endpoints.
        Distinct from Retry on a delivery.
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Replay enqueued
          content:
            application/json:
              example:
                message_id: evt_01HABC
                delivery_ids: [dlv_01HNEW]
                status: enqueued
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/applications:
    get:
      tags: [Applications]
      summary: List applications
      responses:
        "200":
          description: Application list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Application"
              example:
                data:
                  - id: app_01HXYZ
                    name: Acme
                    uid: acme
                    created_at: "2026-09-01T00:00:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Applications]
      summary: Create application
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                uid: { type: string, description: Stable external id }
            example:
              name: Acme
              uid: acme
      responses:
        "201":
          description: Application created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Application"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: uid conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/endpoints:
    get:
      tags: [Endpoints]
      summary: List endpoints
      responses:
        "200":
          description: Endpoint list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Endpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Endpoints]
      summary: Create endpoint
      description: |
        Live requires HTTPS. Test allows localhost.
        Response includes `signing_secret` once (`whsec_`). Store it immediately.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [app_id, url]
              properties:
                app_id: { type: string }
                url: { type: string, format: uri }
                description: { type: string }
                event_types:
                  type: array
                  items: { type: string }
            example:
              app_id: app_01HXYZ
              url: https://example.com/hooks
              event_types: [invoice.paid, invoice.failed]
      responses:
        "201":
          description: Endpoint created (includes signing_secret once)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Endpoint"
                  - type: object
                    properties:
                      signing_secret: { type: string, example: whsec_... }
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/endpoints/{id}/rotate-secret:
    post:
      tags: [Endpoints]
      summary: Rotate endpoint signing secret
      description: Previous secret remains valid until previous_valid_until (dual-sign grace).
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: New signing_secret and previous_valid_until
          content:
            application/json:
              example:
                id: ep_01H
                signing_secret: whsec_new...
                previous_valid_until: "2026-09-06T12:00:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/deliveries/{id}/retry:
    post:
      tags: [Deliveries]
      summary: Retry a delivery
      description: |
        Re-attempt the same delivery to the same endpoint.
        Distinct from message Replay.
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Retry enqueued
          content:
            application/json:
              example:
                delivery_id: dlv_01HDEF
                status: enqueued
                attempt_number: 3
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/event-types:
    get:
      tags: [Event types]
      summary: List event types
      responses:
        "200":
          description: Event catalog
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/EventType"
              example:
                data:
                  - name: invoice.paid
                    description: Invoice settled successfully
                    version: 1
                    example_json: { id: inv_1, amount: 4900 }
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/messages/send-test:
    post:
      tags: [Messages]
      summary: Send a sample test message
      description: |
        Test-only helper. Emits a sample event through the real pipeline.
        Prefer `POST /api/v1/messages` in production. Documented for DX and playgrounds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [app_id]
              properties:
                app_id: { type: string }
                event_type: { type: string, default: invoice.paid }
                payload: { type: object, additionalProperties: true }
            example:
              app_id: app_01HXYZ
              event_type: invoice.paid
      responses:
        "202":
          description: Sample accepted into delivery pipeline
          content:
            application/json:
              example:
                id: evt_01HTEST
                object: message
                status: accepted
                event_type: invoice.paid
                app_id: app_01HXYZ
                delivery_ids: [dlv_01H]
                note: Sample event accepted into the delivery pipeline.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/api-keys:
    post:
      tags: [API keys]
      summary: Create an API key
      description: |
        Creates an additional key for the same org and livemode as the Bearer key.
        The full `key` value is returned once. Use `/api/v1/dev/bootstrap` for the first key in local/dev.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
            example:
              name: CI key
      responses:
        "201":
          description: Key created (plaintext shown once)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyCreated"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/deliveries/{id}:
    get:
      tags: [Deliveries]
      summary: Get a delivery
      description: |
        Returns delivery status and recent attempt summary when available via dashboard parity.
        Public surface for status checks; retry is a separate POST.
      parameters:
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Delivery
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Delivery"
              example:
                id: dlv_01HDEF
                object: delivery
                message_id: evt_01HABC
                endpoint_id: ep_01H
                status: failed
                attempt_count: 2
                next_attempt_at: "2026-09-06T12:05:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_test_ or sk_live_
  parameters:
    Id:
      in: path
      name: id
      required: true
      schema: { type: string }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, example: invalid_request }
            message: { type: string, example: app_id is required }
            request_id: { type: string, example: req_01H }
    EmitMessageRequest:
      type: object
      required: [app_id, event_type, payload]
      properties:
        app_id: { type: string }
        event_type: { type: string }
        payload: { type: object, additionalProperties: true }
        idempotency_key: { type: string }
    EmitMessageResponse:
      type: object
      properties:
        id: { type: string }
        event_type: { type: string }
        app_id: { type: string }
        delivery_ids:
          type: array
          items: { type: string }
        idempotent: { type: boolean }
    Message:
      type: object
      properties:
        id: { type: string }
        event_type: { type: string }
        app_id: { type: string }
        created_at: { type: string, format: date-time }
    Application:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        uid: { type: string }
        created_at: { type: string, format: date-time }
        portal_path: { type: string }
    Endpoint:
      type: object
      properties:
        id: { type: string }
        app_id: { type: string }
        url: { type: string }
        status: { type: string, enum: [active, disabled, failing] }
        event_types:
          type: array
          items: { type: string }
        description: { type: string }

    ApiKeyCreated:
      type: object
      properties:
        id: { type: string }
        object: { type: string, example: api_key }
        name: { type: string }
        prefix: { type: string }
        livemode: { type: boolean }
        key: { type: string, description: Plaintext key, shown once }
    Delivery:
      type: object
      properties:
        id: { type: string }
        object: { type: string, example: delivery }
        message_id: { type: string }
        endpoint_id: { type: string }
        status: { type: string, enum: [pending, success, failed, disabled] }
        attempt_count: { type: integer }
        next_attempt_at: { type: string, format: date-time, nullable: true }
    EventType:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        version: { type: integer }
        example_json: { type: object, additionalProperties: true }
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Too many requests
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: rate_limited
              message: Too many requests
              request_id: req_01H
