{
  "openapi": "3.1.0",
  "info": {
    "title": "HEXSMS API",
    "version": "1.0.0",
    "description": "HEXSMS API: send the same message to up to 1,000 mobile recipients in one country and track each result. Starting rates are 0.02~ USDT and vary by destination. Query enabled countries and account rates before sending. Queued, failed, or canceled messages are not automatically refunded. Carrier integration is configured separately."
  },
  "servers": [
    {
      "url": "https://hexsms.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Messages",
      "description": "벌크 문자 발송과 상태 조회"
    }
  ],
  "paths": {
    "/api/v1/messages": {
      "post": {
        "operationId": "sendMessages",
        "tags": [
          "Messages"
        ],
        "summary": "최대 1,000개 번호로 동일한 문자 발송",
        "description": "API KEY당 발송·조회·갱신 합산 분당 120회. 같은 계정과 요청 키로 같은 정규화 본문을 재시도하면 추가 차감·발송하지 않아요. 다른 본문이면 409예요. 요청 키는 만료하지 않아요. 계정 단가 × 통수 × 번호 수를 접수 시 차감해요. 관리자 계정은 차감하지 않아요.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{8,100}$"
            },
            "description": "작업별 고유 키. 재시도 시 같은 키와 본문을 사용해주세요."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendRequest"
              },
              "example": {
                "country": "US",
                "text": "Your code is 482913.",
                "recipients": [
                  "+12025550123",
                  "+12025550124"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "이미 접수한 요청. replayed=true",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendResponse"
                }
              }
            }
          },
          "202": {
            "description": "발송 큐 등록 완료. 수신 완료를 의미하지 않아요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendResponse"
                }
              }
            }
          },
          "400": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "재시도 전 대기할 초",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/{id}": {
      "get": {
        "operationId": "getMessageStatus",
        "tags": [
          "Messages"
        ],
        "summary": "저장된 번호별 발송 상태 조회",
        "description": "DONE은 큐 처리 완료예요. 수신 완료는 개별 status=DELIVERED로 확인해요. 최신 통신사 로그는 refresh로 가져와요. ID순으로 페이지를 반환해요.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "발송 API에서 받은 캠페인 ID. 같은 계정의 API 발송만 조회해요."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "저장된 전체 요약과 번호별 결과",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            }
          },
          "400": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "재시도 전 대기할 초",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/{id}/refresh": {
      "post": {
        "operationId": "refreshMessageStatus",
        "tags": [
          "Messages"
        ],
        "summary": "통신사 최신 결과로 저장 상태 갱신",
        "description": "본문 없음. 같은 캠페인은 30초 간격. 통신사 로그는 발송 후 48시간 동안 조회할 수 있어요. 이후 GET으로 결과를 읽어주세요. 대기 중이면 updated=0일 수 있어요.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "발송 API에서 받은 캠페인 ID. 같은 계정의 API 발송만 조회해요."
          }
        ],
        "responses": {
          "200": {
            "description": "갱신한 행 수",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "updated"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "updated": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "재시도 전 대기할 초",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/countries": {
      "get": {
        "operationId": "listEnabledCountries",
        "tags": [
          "Messages"
        ],
        "summary": "Get enabled destination countries and account prices",
        "description": "Rates are USDT per segment. Only enabled destinations accept send requests. Account-specific prices take priority.",
        "responses": {
          "200": {
            "description": "Enabled destinations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "currency": {
                      "const": "USDT"
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "country": {
                            "type": "string"
                          },
                          "unitPrice": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "오류 코드와 메시지",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "재시도 전 대기할 초",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "hex_live_<64 hex characters>",
        "description": "로그인 후 /settings/api-keys에서 발급해요. 서버에서만 사용해주세요."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          }
        }
      },
      "SendRequest": {
        "type": "object",
        "required": [
          "text",
          "recipients",
          "country"
        ],
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1600,
            "description": "앞뒤 공백을 제거해요. 모든 대상에 동일한 내용을 발송해요."
          },
          "recipients": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1000,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "maxLength": 30
            },
            "description": "1–1,000 mobile numbers in the selected country. E.164 format is recommended. Invalid or duplicate numbers reject the entire request."
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "Required ISO 3166-1 alpha-2 destination. No default country. All recipients must match this country."
          }
        }
      },
      "Campaign": {
        "type": "object",
        "required": [
          "id",
          "state",
          "total",
          "segments",
          "unitPrice",
          "cost",
          "createdAt",
          "note",
          "country"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "state": {
            "type": "string",
            "enum": [
              "PREPARING",
              "RUNNING",
              "PAUSED",
              "DONE",
              "CANCELED"
            ]
          },
          "total": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "segments": {
            "type": "integer",
            "minimum": 1
          },
          "unitPrice": {
            "type": "number",
            "description": "USDT / segment"
          },
          "cost": {
            "type": "number",
            "description": "발송 접수 시 차감한 USDT"
          },
          "createdAt": {
            "type": "integer",
            "format": "int64",
            "description": "Unix milliseconds"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Z]{2}$"
          }
        }
      },
      "SendResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Campaign"
          },
          {
            "type": "object",
            "required": [
              "replayed"
            ],
            "properties": {
              "replayed": {
                "type": "boolean"
              }
            }
          }
        ]
      },
      "Message": {
        "type": "object",
        "required": [
          "id",
          "to",
          "status",
          "statusName",
          "description",
          "error",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "to": {
            "type": "string",
            "description": "International digits without +"
          },
          "status": {
            "type": "string",
            "description": "QUEUED, SENDING, PENDING, DELIVERED, UNDELIVERABLE, EXPIRED, REJECTED, CANCELED, UNKNOWN 또는 통신사 추가 코드"
          },
          "statusName": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "updatedAt": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "StatusResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Campaign"
          },
          {
            "type": "object",
            "required": [
              "summary",
              "messages",
              "pagination"
            ],
            "properties": {
              "summary": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "messages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Message"
                }
              },
              "pagination": {
                "type": "object",
                "required": [
                  "limit",
                  "offset",
                  "nextOffset"
                ],
                "properties": {
                  "limit": {
                    "type": "integer"
                  },
                  "offset": {
                    "type": "integer"
                  },
                  "nextOffset": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}
