openapi: 3.0.3
info:
  title: TopSMS REST API
  version: "1.3"
  description: |
    SMS gateway — send SMS (also in bulk via a queue), delivery status, credit balance
    and a Verify API for one-time passcodes (OTP).
    Documentation: https://topsms.eu/docs (English), https://topsms.eu/de/docs (German), https://www.topsms.cz/api-integrace/rest-api (Czech).
    Test keys (a checkbox in the dashboard) send nothing and charge nothing; responses carry the field test: true
    and a simulated delivery receipt arrives after 2–4 s (number ending 0000 = failed, 0001 = expired).
    Scheduled sending: the send_at field (ISO 8601 with time zone, max 30 days ahead); cancel with DELETE /api/sms/jobs/{id}.
    Idempotency: the Idempotency-Key header (24 h) on the sending endpoints; a repeated request does not send the SMS again.
    Delivery failure reason: the errorCode field (expired, undelivered, rejected, deleted, unknown, repeated_failure, not_sent).
    Authentication: header "Authorization: Bearer <clientId>:<secret>".
    Keys are managed in the dashboard (https://www.topsms.cz/dashboard/api).
    Push delivery receipts: a webhook URL can be set on the API key — whenever a message's
    status changes we send an HTTP POST (payload: see the WebhookEvent component, signature
    in the X-TopSMS-Signature header: sha256=HMAC-SHA256(body, webhookSecret),
    3 attempts: immediately, +30 s, +2 min).
    Ready-made no-code integrations: WooCommerce plugin (https://www.topsms.cz/api-integrace/woocommerce),
    SMS invoice reminders from Fakturoid and iDoklad (https://www.topsms.cz/reseni/sms-upominky-faktur),
    Make/Zapier/n8n via an HTTP module; a Shoptet add-on is in preparation (https://www.topsms.cz/api-integrace/shoptet).
    Overview: https://www.topsms.cz/api-integrace/pluginy
  contact:
    email: info@topsms.cz
    url: https://topsms.eu/docs
servers:
  - url: https://www.topsms.cz
security:
  - bearerAuth: []
paths:
  /api/sms/send:
    post:
      summary: Send an SMS, optionally scheduled (scope send)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to, text]
              properties:
                to: { type: string, example: "+420601234567", description: "Phone number in international format" }
                text: { type: string, example: "Vase objednavka je na ceste.", description: "Message text — sent verbatim, the API does not substitute placeholders such as {name}" }
                from: { type: string, maxLength: 11, example: "MojeFirma", description: "Approved Sender ID. If omitted, the account's default sender is used (Dashboard → Sender ID → Set as default), otherwise TopSMS" }
                send_at: { $ref: "#/components/schemas/SendAt" }
      responses:
        "200":
          description: Sent, credit deducted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  id: { type: string, description: "Internal message ID (for status lookups)" }
                  externalId: { type: string, nullable: true }
                  to: { type: string }
                  from: { type: string }
                  smsCount: { type: integer, description: "Number of billed SMS segments (1 segment = 160 characters without diacritics, 70 characters with diacritics; longer texts 153/67 characters per segment). See https://www.topsms.cz/cenik#segment" }
                  price: { type: number, description: "Price in CZK" }
                  status: { type: string, example: sent }
                  test: { type: boolean, description: "true for a test key (nothing sent or charged)" }
                  senderFallback: { type: boolean, description: "true when the Sender ID is approved but not yet active with the carriers; the message went out as TopSMS" }
                  requestedFrom: { type: string, description: "Requested Sender ID (only with senderFallback)" }
        "202":
          description: Scheduled (send_at), credit deducted; cancel with DELETE /api/sms/jobs/{jobId}
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  scheduled: { type: boolean, example: true }
                  id: { type: string }
                  jobId: { type: string }
                  statusUrl: { type: string }
                  status: { type: string, example: scheduled }
                  sendAt: { type: string, format: date-time }
                  smsCount: { type: integer }
                  price: { type: number }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "500": { $ref: "#/components/responses/Error" }
        "502": { $ref: "#/components/responses/Error" }
  /api/sms/send-bulk:
    post:
      summary: Send SMS in bulk, up to 500 recipients, optionally scheduled (scope send)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                to: { type: array, items: { type: string }, description: "Recipients for a shared text", example: ["+420601234567"] }
                text: { type: string, description: "Shared text for all recipients in the to array" }
                messages:
                  type: array
                  description: "Alternative to to+text — a custom text for each recipient"
                  items:
                    type: object
                    required: [to, text]
                    properties:
                      to: { type: string }
                      text: { type: string }
                from: { type: string, maxLength: 11, description: "Approved Sender ID. If omitted, the account's default sender is used (Dashboard → Sender ID → Set as default), otherwise TopSMS" }
                send_at: { $ref: "#/components/schemas/SendAt" }
                async: { type: boolean, description: "true = process via the queue even below 50 recipients (202 response with jobId)" }
      responses:
        "200":
          description: Batch processed; the result is listed for each recipient separately
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  total: { type: integer }
                  accepted: { type: integer, description: "Number of sent and billed messages" }
                  rejected: { type: integer }
                  price: { type: number, description: "Total deducted in CZK" }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        to: { type: string }
                        status: { type: string, enum: [sent, failed] }
                        id: { type: string, nullable: true }
                        externalId: { type: string, nullable: true }
                        price: { type: number }
                        error: { type: string, nullable: true }
        "202":
          description: "A batch of more than 50 recipients (or with "async": true) was accepted into the queue; credit is deducted and a worker sends the messages. Track progress via GET /api/sms/jobs/{jobId} or webhooks."
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  queued: { type: boolean, example: true }
                  jobId: { type: string }
                  scheduled: { type: boolean, description: "true when send_at was given" }
                  sendAt: { type: string, format: date-time }
                  statusUrl: { type: string, example: "https://www.topsms.cz/api/sms/jobs/cmg..." }
                  total: { type: integer }
                  accepted: { type: integer, description: "Number of messages placed in the queue" }
                  rejected: { type: integer }
                  price: { type: number }
                  results: { type: array, items: { type: object } }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /api/sms/jobs/{id}:
    get:
      summary: Status of a queued batch or a scheduled send (scope read)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: jobId from the 202 response of send-bulk
        - name: results
          in: query
          required: false
          schema: { type: string, enum: ["0", "1"] }
          description: "0 = without the list of messages (counters only)"
      responses:
        "200":
          description: Batch progress
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  status: { type: string, enum: [scheduled, queued, running, done, failed, cancelled] }
                  sendAt: { type: string, format: date-time, nullable: true }
                  total: { type: integer }
                  sent: { type: integer }
                  failed: { type: integer }
                  queued: { type: integer }
                  price: { type: number, description: "Deducted price after refunds" }
                  createdAt: { type: string, format: date-time }
                  startedAt: { type: string, format: date-time, nullable: true }
                  finishedAt: { type: string, format: date-time, nullable: true }
                  error: { type: string, nullable: true }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        to: { type: string }
                        status: { type: string, enum: [queued, sent, delivered, failed, expired] }
                        externalId: { type: string, nullable: true }
                        price: { type: number }
                        error: { type: string, nullable: true }
                        errorCode: { $ref: "#/components/schemas/ErrorCode" }
        "404": { $ref: "#/components/responses/Error" }
    delete:
      summary: Cancel a scheduled send or a batch that has not started sending yet (scope send)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Cancelled, price refunded to credit
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  id: { type: string }
                  status: { type: string, example: cancelled }
                  cancelled: { type: integer, description: "Number of cancelled messages" }
                  refunded: { type: number, description: "Refunded to credit in CZK" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
  /api/verify/send:
    post:
      summary: Send a verification code by SMS (scope send)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      description: Generates a code, sends it by SMS and returns the verification id. A test key sends nothing and returns testCode.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to]
              properties:
                to: { type: string, example: "+420601234567" }
                brand: { type: string, maxLength: 20, example: "MojeFirma" }
                length: { type: integer, minimum: 4, maximum: 8, default: 6 }
                ttl: { type: integer, minimum: 60, maximum: 1800, default: 300, description: "Validity in seconds" }
                locale: { type: string, enum: [cs, en], default: cs }
                from: { type: string, maxLength: 11, description: "Approved Sender ID" }
      responses:
        "200":
          description: Code sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  id: { type: string }
                  to: { type: string }
                  status: { type: string, example: pending }
                  expiresAt: { type: string, format: date-time }
                  messageId: { type: string, nullable: true }
                  price: { type: number }
                  test: { type: boolean }
                  testCode: { type: string, description: "Only with a test key" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "502": { $ref: "#/components/responses/Error" }
  /api/verify/check:
    post:
      summary: Check a verification code (scope send)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                id: { type: string, description: "id from /api/verify/send" }
                to: { type: string, description: "Alternative to id: the latest code sent to this number" }
                code: { type: string, example: "482913" }
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid: { type: boolean }
                  status: { type: string, enum: [approved, pending, expired, failed, canceled] }
                  attemptsLeft: { type: integer }
                  id: { type: string }
        "404": { $ref: "#/components/responses/Error" }
  /api/sms/status/{id}:
    get:
      summary: Delivery status of a message (scope read)
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: Internal id or externalId from the send response
      responses:
        "200":
          description: Message detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  externalId: { type: string, nullable: true }
                  to: { type: string }
                  from: { type: string }
                  text: { type: string }
                  status: { type: string, enum: [queued, sent, delivered, failed, expired, pending] }
                  price: { type: number }
                  carrier: { type: string, nullable: true, description: "Network code MCC.MNC, e.g. 230.1 (T-Mobile CZ)" }
                  createdAt: { type: string, format: date-time }
                  deliveredAt: { type: string, format: date-time, nullable: true }
                  failedAt: { type: string, format: date-time, nullable: true }
                  errorMessage: { type: string, nullable: true }
                  errorCode: { $ref: "#/components/schemas/ErrorCode" }
        "404": { $ref: "#/components/responses/Error" }
  /api/credit:
    get:
      summary: Credit balance (scope read)
      responses:
        "200":
          description: Current balance
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  credit: { type: number, description: "Balance in CZK" }
                  currency: { type: string, example: CZK }
                  smsPrice: { type: number, description: "Price per SMS according to your plan" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Bearer <clientId>:<secret>"
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 255 }
      description: "1–255 printable ASCII characters without spaces (otherwise 400 invalid_idempotency_key). A repeated request with the same key (24 h) does not send the SMS again; it returns the original response with the header Idempotent-Replayed: true. Different body = 422, concurrent run = 409. Responses 401, 402, 403, 408, 409, 429 and 5xx are not stored, except 500 record_failed."
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
              code: { type: string, description: "Machine-readable error code (e.g. insufficient_credit, invalid_send_at, invalid_idempotency_key, idempotency_mismatch, idempotency_in_progress, not_cancellable, campaign_job, record_failed = SMS sent but not recorded, credit refunded, do not retry)" }
  schemas:
    SendAt:
      type: string
      format: date-time
      example: "2026-10-01T09:00:00+02:00"
      description: "Scheduled sending: ISO 8601 with time zone (or a Unix timestamp), max 30 days ahead. Returns 202 with jobId."
    ErrorCode:
      type: string
      nullable: true
      enum: [expired, undelivered, rejected, deleted, unknown, repeated_failure, not_sent]
      description: "Delivery failure reason; repeated_failure = delivery to the same number has failed at least three times within 90 days"
    WebhookEvent:
      type: object
      description: Payload of the HTTP POST to the webhook URL when a message's status changes
      properties:
        event: { type: string, example: sms.status }
        id: { type: string }
        externalId: { type: string, nullable: true }
        to: { type: string }
        from: { type: string }
        status: { type: string, enum: [delivered, failed, expired] }
        price: { type: number }
        deliveredAt: { type: string, format: date-time, nullable: true }
        failedAt: { type: string, format: date-time, nullable: true }
        errorMessage: { type: string, nullable: true }
        errorCode: { $ref: "#/components/schemas/ErrorCode" }
        sentAt: { type: string, format: date-time }
        test: { type: boolean, description: "true for messages sent with a test key and for the test webhook" }
