openapi: 3.1.0
info:
  title: Bulko API
  version: 1.0.0
  summary: Campaign automation API for Bulko customers
  description: |
    Create managed email campaigns, import subscriber lists, read campaign
    statistics and replies, and inspect account balances.

    API keys are issued in the Bulko dashboard and are shown only once. Keep
    them server-side. All sending is subject to Bulko's Acceptable Use Policy,
    suppression checks, content safety controls, and available email credits.
  contact:
    name: Bulko Support
    url: https://bulko.io/en/contact/
    email: support@bulko.io
  license:
    name: Bulko Terms of Service
    url: https://bulko.io/en/terms-of-service/
servers:
  - url: https://bulko.io/api/v1
    description: Production
externalDocs:
  description: API and integrations guide
  url: https://bulko.io/en/integrations/?utm_source=postman&utm_medium=api&utm_campaign=bulko-api
security:
  - bearerAuth: []
tags:
  - name: Campaigns
    description: Create campaigns and inspect delivery and engagement statistics.
  - name: Subscribers
    description: Import and maintain campaign recipient lists.
  - name: Replies
    description: Read classified replies and bounces from owned campaigns.
  - name: Account
    description: Inspect available credits and lifetime usage.
  - name: SMTP Access
    description: Inspect separately purchased SMTP relay access and limits.
paths:
  /campaigns/launch:
    post:
      tags: [Campaigns]
      summary: Create and queue a campaign
      operationId: launchCampaign
      description: |
        Creates a campaign and queues eligible recipients for delivery. Globally
        suppressed addresses are removed automatically. If the account has fewer
        credits than recipients, Bulko sends only the available-credit portion and
        pauses the remainder.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LaunchCampaignRequest'
            examples:
              recipients:
                summary: Launch with inline recipients
                value:
                  name: Product update
                  subject: A useful update for your team
                  body: '<p>Hello, here is the update you requested.</p>'
                  recipients: [alex@example.com, sam@example.com]
                  reply_to: replies@example.com
                  language: en
                  enable_tracking: true
              subscriberList:
                summary: Launch from an imported list
                value:
                  name: Customer onboarding
                  subject: Getting started
                  body: '<p>Welcome. Here are your next steps.</p>'
                  subscriber_list_id: 42
                  language: en
      responses:
        '202':
          description: Campaign accepted and queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LaunchCampaignResponse'
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '402': {$ref: '#/components/responses/InsufficientCredits'}
        '403': {$ref: '#/components/responses/ContentBlocked'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /campaigns/{campaign_id}/stats:
    get:
      tags: [Campaigns]
      summary: Get campaign statistics
      operationId: getCampaignStats
      parameters:
        - $ref: '#/components/parameters/CampaignId'
      responses:
        '200':
          description: Campaign statistics
          content:
            application/json:
              schema:
                type: object
                required: [success, campaign]
                properties:
                  success: {type: boolean, const: true}
                  campaign:
                    $ref: '#/components/schemas/CampaignStats'
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /subscribers/import:
    post:
      tags: [Subscribers]
      summary: Import subscribers into a list
      operationId: importSubscribers
      description: |
        Imports up to 10,000 addresses. Invalid, duplicate, and globally
        suppressed addresses are skipped and counted in the response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportSubscribersRequest'
            example:
              list_name: Webinar attendees
              emails:
                - email: alex@example.com
                  name: Alex
                - sam@example.com
      responses:
        '200':
          description: Import result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSubscribersResponse'
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /replies:
    get:
      tags: [Replies]
      summary: List replies
      operationId: listReplies
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, positive, negative, manual_review, unmatched, bounce]
        - name: limit
          in: query
          schema: {type: integer, minimum: 1, maximum: 100, default: 50}
        - name: offset
          in: query
          schema: {type: integer, minimum: 0, default: 0}
      responses:
        '200':
          description: Paginated replies
          content:
            application/json:
              schema:
                type: object
                required: [success, total, limit, offset, items]
                properties:
                  success: {type: boolean, const: true}
                  total: {type: integer}
                  limit: {type: integer}
                  offset: {type: integer}
                  items:
                    type: array
                    items: {$ref: '#/components/schemas/Reply'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /account/balance:
    get:
      tags: [Account]
      summary: Get credit balances
      operationId: getAccountBalance
      responses:
        '200':
          description: Current balances and usage
          content:
            application/json:
              schema:
                type: object
                required: [success, balance]
                properties:
                  success: {type: boolean, const: true}
                  balance:
                    type: object
                    required: [email_credits, ai_credits, unlimited, total_emails_sent]
                    properties:
                      email_credits: {type: integer, minimum: 0}
                      ai_credits: {type: integer, minimum: 0}
                      unlimited: {type: boolean}
                      total_emails_sent: {type: integer, minimum: 0}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '429': {$ref: '#/components/responses/RateLimited'}
  /smtp/accesses:
    get:
      tags: [SMTP Access]
      summary: List SMTP Access packages and usage
      operationId: listSmtpAccesses
      responses:
        '200':
          description: SMTP Access records owned by the authenticated user
          content:
            application/json:
              schema:
                type: object
                required: [success, items]
                properties:
                  success: {type: boolean, const: true}
                  items:
                    type: array
                    items: {$ref: '#/components/schemas/SmtpAccess'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '429': {$ref: '#/components/responses/RateLimited'}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Bulko API key
      description: API key beginning with `bulko_live_`.
  parameters:
    CampaignId:
      name: campaign_id
      in: path
      required: true
      schema: {type: integer, minimum: 1}
  schemas:
    LaunchCampaignRequest:
      type: object
      required: [name, subject, body]
      oneOf:
        - required: [recipients]
        - required: [subscriber_list_id]
      properties:
        name: {type: string, minLength: 1, maxLength: 100}
        subject: {type: string, minLength: 1, maxLength: 255}
        body: {type: string, minLength: 1}
        recipients:
          type: array
          items:
            oneOf:
              - {type: string, format: email}
              - type: object
                required: [email]
                properties:
                  email: {type: string, format: email}
        subscriber_list_id: {type: integer, minimum: 1}
        reply_to: {type: string, format: email}
        language: {type: string, enum: [en, ru], default: en}
        enable_tracking: {type: boolean, default: true}
        ai_rotation: {type: boolean, default: false}
        ai_rotation_interval: {type: integer, minimum: 1, maximum: 100000, default: 100}
        ai_base_prompt: {type: string}
    LaunchCampaignResponse:
      type: object
      required: [success, id, status, recipients, suppressed, stats_url]
      properties:
        success: {type: boolean, const: true}
        id: {type: integer}
        status: {type: string}
        recipients: {type: integer}
        suppressed: {type: integer}
        stats_url: {type: string}
    CampaignStats:
      type: object
      properties:
        id: {type: integer}
        name: {type: string}
        status: {type: string}
        recipients: {type: integer}
        sent: {type: integer}
        failed: {type: integer}
        opened: {type: integer}
        clicked: {type: integer}
        replies: {type: integer}
        bounces: {type: integer}
        unsubscribed: {type: integer}
        created_at: {type: string, format: date-time}
        completed_at: {type: [string, 'null'], format: date-time}
    ImportSubscribersRequest:
      type: object
      required: [emails]
      oneOf:
        - required: [list_name]
        - required: [subscriber_list_id]
      properties:
        list_name: {type: string, minLength: 1, maxLength: 100}
        subscriber_list_id: {type: integer, minimum: 1}
        emails:
          type: array
          minItems: 1
          maxItems: 10000
          items:
            oneOf:
              - {type: string, format: email}
              - type: object
                required: [email]
                properties:
                  email: {type: string, format: email}
                  name: {type: string, maxLength: 255}
    ImportSubscribersResponse:
      type: object
      required: [success, list]
      properties:
        success: {type: boolean, const: true}
        list:
          type: object
          properties:
            id: {type: integer}
            name: {type: string}
            added: {type: integer}
            invalid: {type: integer}
            duplicates: {type: integer}
            suppressed: {type: integer}
            total: {type: integer}
    Reply:
      type: object
      properties:
        id: {type: integer}
        campaign_id: {type: [integer, 'null']}
        campaign_name: {type: [string, 'null']}
        from: {type: string, format: email}
        subject: {type: string}
        body: {type: string}
        status: {type: string}
        confidence: {type: [number, 'null']}
        sentiment: {type: [string, 'null']}
        is_bounce: {type: boolean}
        received_at: {type: string, format: date-time}
    SmtpAccess:
      type: object
      properties:
        id: {type: integer}
        host: {type: string, const: smtp.bulko.io}
        port: {type: integer, const: 587}
        encryption: {type: string, const: STARTTLS}
        username: {type: string}
        active: {type: boolean}
        email_limit: {type: integer}
        emails_sent: {type: integer}
        emails_remaining: {type: integer}
        expires_at: {type: [string, 'null'], format: date-time}
    ApiError:
      type: object
      required: [success, error]
      properties:
        success: {type: boolean, const: false}
        error:
          type: object
          required: [code, message]
          properties:
            code: {type: string}
            message: {type: string}
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ApiError'}
    Unauthorized:
      description: Missing, invalid, or revoked API key
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ApiError'}
    InsufficientCredits:
      description: No email credits are available
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ApiError'}
    ContentBlocked:
      description: Content was rejected by the safety filter
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ApiError'}
    NotFound:
      description: The requested owned resource was not found
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ApiError'}
    RateLimited:
      description: More than 60 requests were made in one minute
      headers:
        Retry-After:
          schema: {type: integer}
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ApiError'}
