openapi: 3.1.0
info:
  title: subdomain.to API
  version: 1.0.0
  description: Connect customer-owned custom domains directly to HTTPS destinations.
servers:
  - url: https://api.subdomain.to/v1
security:
  - bearerAuth: []
paths:
  /health:
    get:
      security: []
      operationId: health
      responses:
        '200': {description: Healthy}
  /destinations:
    get:
      operationId: listDestinations
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}]
      responses:
        '200': {description: HTTPS destinations}
    post:
      operationId: createDestination
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, required: [url], properties: {url: {type: string, format: uri}}}
            examples: {default: {value: {url: 'https://app.monsaas.com'}}}
      responses:
        '201': {description: Destination created}
        '400': {$ref: '#/components/responses/Error'}
  /domains:
    get:
      operationId: listDomains
      parameters: [{$ref: '#/components/parameters/Limit'}, {$ref: '#/components/parameters/Cursor'}]
      responses:
        '200': {description: Customer domains}
    post:
      operationId: createDomain
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [hostname]
              properties:
                hostname: {type: string, example: app.client.com}
                destination_id: {type: string, description: Optional; the account's default destination is used when omitted.}
      responses:
        '201': {description: Domain created, content: {application/json: {schema: {$ref: '#/components/schemas/DomainEnvelope'}}}}
        '409': {$ref: '#/components/responses/Error'}
  /domains/{id}:
    get:
      operationId: getDomain
      parameters: [{$ref: '#/components/parameters/ResourceId'}]
      responses:
        '200': {description: Domain state, content: {application/json: {schema: {$ref: '#/components/schemas/DomainEnvelope'}}}}
        '404': {$ref: '#/components/responses/Error'}
    delete:
      operationId: deleteDomain
      parameters: [{$ref: '#/components/parameters/ResourceId'}]
      responses:
        '202': {description: Deletion scheduled}
  /webhook-endpoints:
    get:
      operationId: listWebhookEndpoints
      responses: {'200': {description: Webhook endpoints}}
    post:
      operationId: createWebhookEndpoint
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody:
        required: true
        content: {application/json: {schema: {type: object, required: [url], properties: {url: {type: string, format: uri}, events: {type: array, items: {type: string}}}}}}
      responses: {'201': {description: Endpoint created; secret is returned once}}
  /webhook-endpoints/{id}:
    patch:
      operationId: updateWebhookEndpoint
      parameters: [{$ref: '#/components/parameters/ResourceId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
      responses: {'200': {description: Endpoint updated}}
    delete:
      operationId: deleteWebhookEndpoint
      parameters: [{$ref: '#/components/parameters/ResourceId'}]
      responses: {'204': {description: Endpoint deleted}}
  /webhook-deliveries:
    get:
      operationId: listWebhookDeliveries
      responses: {'200': {description: Recent deliveries}}
  /webhook-deliveries/{id}/retry:
    post:
      operationId: retryWebhookDelivery
      parameters: [{$ref: '#/components/parameters/ResourceId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
      responses: {'202': {description: Retry queued}}
  /widget-sessions:
    post:
      operationId: createWidgetSession
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody:
        required: true
        content: {application/json: {schema: {type: object, properties: {external_customer_id: {type: string}, allowed_origins: {type: array, items: {type: string, format: uri}}}}}}
      responses: {'201': {description: 30-minute browser token}}
  /widget/domains:
    post:
      security: [{widgetAuth: []}]
      operationId: createWidgetDomain
      description: Create a customer domain using a widget session token.
      requestBody:
        required: true
        content: {application/json: {schema: {type: object, required: [hostname], properties: {hostname: {type: string, example: portal.customer.com}}}}}
      responses:
        '201': {description: Domain created with DNS records, content: {application/json: {schema: {$ref: '#/components/schemas/DomainEnvelope'}}}}
        '401': {$ref: '#/components/responses/Error'}
        '403': {$ref: '#/components/responses/Error'}
        '409': {$ref: '#/components/responses/Error'}
  /widget/domains/{id}:
    get:
      security: [{widgetAuth: []}]
      operationId: getWidgetDomain
      parameters: [{$ref: '#/components/parameters/ResourceId'}]
      responses:
        '200': {description: Domain state, content: {application/json: {schema: {$ref: '#/components/schemas/DomainEnvelope'}}}}
        '404': {$ref: '#/components/responses/Error'}
  /widget/domains/{id}/verify:
    post:
      security: [{widgetAuth: []}]
      operationId: verifyWidgetDomain
      parameters: [{$ref: '#/components/parameters/ResourceId'}]
      responses:
        '202': {description: DNS verification queued, content: {application/json: {schema: {$ref: '#/components/schemas/DomainEnvelope'}}}}
        '404': {$ref: '#/components/responses/Error'}
  /usage:
    get:
      operationId: getUsage
      responses: {'200': {description: Current cycle usage and included limits}}
  /billing/checkout-session:
    post:
      operationId: createCheckoutSession
      responses: {'201': {description: Stripe Checkout session created}}
  /billing/portal-session:
    post:
      operationId: createBillingPortalSession
      responses: {'201': {description: Stripe Billing Portal session created}}
  /billing/plan:
    post:
      operationId: changeBillingPlan
      responses: {'200': {description: Subscription plan changed or scheduled}}
components:
  securitySchemes:
    bearerAuth: {type: http, scheme: bearer, bearerFormat: API key}
    widgetAuth: {type: http, scheme: bearer, bearerFormat: JWT, description: 30-minute widget session token}
  parameters:
    IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 1, maxLength: 128}}
    ResourceId: {name: id, in: path, required: true, schema: {type: string}}
    Limit: {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 25}}
    Cursor: {name: cursor, in: query, schema: {type: string}}
  schemas:
    Destination: {type: object, required: [id, url, host, port], properties: {id: {type: string}, url: {type: string, format: uri}, host: {type: string}, port: {type: integer, enum: [443, 8443]}}}
    DomainEnvelope: {type: object, required: [data], properties: {data: {$ref: '#/components/schemas/Domain'}}}
    Domain:
      type: object
      required: [id, hostname, type, status, dns_status, certificate_status, routing_status]
      properties:
        id: {type: string}
        hostname: {type: string}
        external_customer_id: {type: [string, 'null']}
        type: {type: string, enum: [exact, apex]}
        status: {type: string, enum: [pending_dns, validating, provisioning, active, misconfigured, failed, suspended, deleting]}
        dns_status: {type: string}
        certificate_status: {type: string}
        routing_status: {type: string}
        dns_records: {type: array, items: {$ref: '#/components/schemas/DnsRecord'}}
        origin: {$ref: '#/components/schemas/Destination'}
    DnsRecord: {type: object, required: [type, name, value], properties: {type: {type: string, enum: [A, CNAME]}, name: {type: string}, value: {type: string}}}
    Error: {type: object, required: [error], properties: {error: {type: object, required: [code, message], properties: {code: {type: string}, message: {type: string}, request_id: {type: [string, 'null']}}}}}
  responses:
    Error: {description: Structured error, content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}}

# Quick start
# curl -X POST https://api.subdomain.to/v1/domains \
#   -H "Authorization: Bearer $SUBDOMAIN_TO_API_KEY" -H "Idempotency-Key: customer-42-domain-1" \
#   -H "Content-Type: application/json" -d '{"hostname":"app.client.com"}'
