Crescendo Lab SMS API

Transactional (1-to-1) SMS send + status.

OpenAPI Specification

crescendo-lab-sms-api-openapi.yml Raw ↑
openapi: 3.0.3
info:
  title: MAAC Go Broadcast SMS API
  version: 1.0.0
  description: "MAAC Go — Taiwan-first self-serve SMS platform. Send transactional (OTP /\norder / reminder) and broadcast SMS with NCC compliance, MAAC delivery\nbackend, and real-time DLR webhooks.\n\n- Base URL: `https://sms.cresclab.com/api`\n- Auth: `Authorization: Bearer <sk_live_... or sk_test_...>`\n- New accounts receive NT$50 trial credit on signup, enough to send real test SMS immediately.\n- API keys are scoped and wallet-backed; sending debits the account balance unless your deployment uses a mock SMS/payment adapter.\n- All phone numbers use E.164 (`+886912345678`). 09xxxxxxxx is also accepted.\n- Cost: NT$0.78 per SMS segment (Chinese: 70 chars/segment · English: 160/segment).\n- Rate-limit errors return `429 rate_limited` with limit/used details and retry guidance when available.\n- Delivery confirmations arrive asynchronously via webhook (`sms.delivered`\n  / `sms.failed`); poll `GET /sms/{id}` as a fallback.\n- MCP clients (Claude / Cursor / Windsurf) can use this spec directly, or\n  connect via `POST /api/mcp` (JSON-RPC 2.0) with the same bearer key.\n"
  contact:
    name: MAAC Go Support
    email: info@cresclab.com
    url: https://sms.cresclab.com
  license:
    name: Commercial
    url: https://sms.cresclab.com/terms.html
servers:
- url: https://sms.cresclab.com/api
  description: Production
security:
- bearerAuth: []
tags:
- name: SMS
  description: Transactional (1-to-1) SMS send + status.
paths:
  /sms/send:
    post:
      tags:
      - SMS
      summary: Send a transactional SMS (1-to-1)
      description: 'OTP, order notifications, appointment reminders. High-priority channel,

        no LINE fallback. Returns after MAAC accepts the request; final delivery

        status arrives via the `sms.delivered` / `sms.failed` webhook.

        '
      operationId: sendSms
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SmsSendRequest'
            examples:
              otp:
                value:
                  to: '+886912345678'
                  body: 【YourBrand】驗證碼 483291,5 分鐘內有效。勿告知他人。
                  type: otp
              notification:
                value:
                  to: '+886912345678'
                  body: 【YourBrand】訂單 A12345 已出貨,預計 3/15 送達。
                  type: notification
      responses:
        '200':
          description: Queued for delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmsSendResponse'
        '400':
          description: Validation or NCC compliance failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                ncc:
                  value:
                    error: ncc_blocked
                    issues:
                    - level: block
                      code: SHORTENER
                      reason: bit.ly is on NCC 2024/11 blocklist
        '401':
          description: Auth missing / invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '402':
          description: Wallet balance insufficient
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /sms/{id}:
    get:
      tags:
      - SMS
      summary: Retrieve SMS status
      description: Poll once per 5+ seconds as a fallback; prefer the `sms.delivered` webhook.
      operationId: getSms
      parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
        example: sms_abc123
      responses:
        '200':
          description: Current message state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SmsDetail'
        '404':
          description: Not found
  /sms/list:
    get:
      tags:
      - SMS
      summary: List recent messages (Resend-style delivery log)
      operationId: listSms
      parameters:
      - in: query
        name: limit
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
      - in: query
        name: status
        schema:
          type: string
          enum:
          - queued
          - sent
          - delivered
          - failed
          - stop
      - in: query
        name: team
        description: Filter to one cost-attribution team. Use the literal `(未分類)` for untagged messages.
        schema:
          type: string
        example: 付費會員
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  messages:
                    type: array
                    items:
                      $ref: '#/components/schemas/SmsDetail'
                  summary_7d:
                    type: object
                    additionalProperties:
                      type: integer
  /sms/by-team:
    get:
      tags:
      - SMS
      summary: Cost attribution per team (messages / cost / delivered / failed)
      description: 'Returns one row per team plus `(未分類)` for untagged sends, so the rows sum to the

        account total. Money is shared (one wallet, one invoice per 統編); this is a reporting

        breakdown for internal chargeback, **not** a wallet split.


        Windowing: pass `from`/`to` for an exact billing period (monthly chargeback), or `days`

        for a rolling window. `from`/`to` take precedence when supplied.

        '
      operationId: smsByTeam
      parameters:
      - in: query
        name: days
        description: Rolling window in days (ignored if from/to given).
        schema:
          type: integer
          default: 30
          maximum: 90
      - in: query
        name: from
        description: Start of an exact range (ISO date/datetime, inclusive).
        schema:
          type: string
          format: date-time
        example: '2026-06-01'
      - in: query
        name: to
        description: End of an exact range (ISO date/datetime, exclusive).
        schema:
          type: string
          format: date-time
        example: '2026-07-01'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  days:
                    type: integer
                    description: Present in rolling-window mode
                  from:
                    type: string
                    nullable: true
                    description: Present in range mode
                  to:
                    type: string
                    nullable: true
                    description: Present in range mode
                  total_cost_cents:
                    type: integer
                  teams:
                    type: array
                    items:
                      type: object
                      properties:
                        team:
                          type: string
                          description: Team name, or (未分類) for untagged
                        msgs:
                          type: integer
                        cost_cents:
                          type: integer
                        delivered:
                          type: integer
                        failed:
                          type: integer
  /sms/metrics:
    get:
      tags:
      - SMS
      summary: Daily volume + delivery metrics
      operationId: getMetrics
      parameters:
      - in: query
        name: days
        schema:
          type: integer
          default: 30
          maximum: 90
      responses:
        '200':
          description: Aggregated metrics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MetricsResponse'
components:
  schemas:
    SmsDetail:
      type: object
      properties:
        id:
          type: string
        to:
          type: string
        body:
          type: string
        status:
          type: string
          enum:
          - queued
          - sent
          - delivered
          - failed
          - stop
        segments:
          type: integer
        cost_cents:
          type: integer
        sent_at:
          type: string
          format: date-time
          nullable: true
        delivered_at:
          type: string
          format: date-time
          nullable: true
        error:
          type: string
          nullable: true
        gateway_ref:
          type: string
          nullable: true
          description: Upstream MAAC pnp_message_id
    SmsSendResponse:
      type: object
      properties:
        ok:
          type: boolean
        message_id:
          type: string
          example: sms_abc123
        status:
          type: string
          enum:
          - queued
          - sent
          - delivered
          - failed
        segments:
          type: integer
        cost_cents:
          type: integer
          description: Amount debited in NT cents (78 = NT$0.78)
        balance_cents:
          type: integer
        team:
          type: string
          nullable: true
          description: Resolved cost-attribution team (null = 未分類). Echo of what was tagged.
        team_created:
          type: boolean
          description: True if this send auto-created the team (first use of the name).
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          example: ncc_blocked
        hint:
          type: string
        issues:
          type: array
          items:
            type: object
            properties:
              level:
                type: string
                enum:
                - block
                - warn
              code:
                type: string
              reason:
                type: string
    SmsSendRequest:
      type: object
      required:
      - to
      - body
      properties:
        to:
          type: string
          description: E.164 (`+886912345678`) or TW local (`0912345678`)
          example: '+886912345678'
        body:
          type: string
          maxLength: 1000
          description: SMS text. 70 chars = 1 segment (Chinese), 160 (ASCII). Must include `【brand】` prefix and `STOP` / `退訂` for marketing.
        from:
          type: string
          description: Sender ID. Defaults to Cresclab shared short-code `1990`.
        type:
          type: string
          enum:
          - otp
          - notification
          - marketing
          description: Controls routing priority and compliance checks.
        team:
          type: string
          description: Cost-attribution team for this send (e.g. `付費會員`). Overrides the API key's bound team. An unknown name is auto-created, so a typo surfaces as a stray team rather than vanishing. Omit to inherit the key's team or fall to 未分類.
          example: 付費會員
    MetricsResponse:
      type: object
      properties:
        ok:
          type: boolean
        days:
          type: integer
        daily:
          type: array
          items:
            type: object
            properties:
              day:
                type: string
                format: date
              total:
                type: integer
              delivered:
                type: integer
              failed:
                type: integer
              cost_cents:
                type: integer
        totals:
          type: object
          properties:
            total:
              type: integer
            delivered:
              type: integer
            failed:
              type: integer
            cost_cents:
              type: integer
            avg_latency_sec:
              type: number
              nullable: true
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: '`Authorization: Bearer <api_key>`. Production keys start with `sk_live_`,

        test keys with `sk_test_` for environment separation. Sending still follows

        the account wallet and deployment adapter settings.

        '