{
  "openapi": "3.1.0",
  "info": {
    "title": "Gravity SMS Gateway API",
    "version": "1.0.0",
    "description": "REST API for sending and receiving SMS via RingCentral. Multi-tenant; each tenant connects a RingCentral account and uses API keys to send SMS, check status, manage apps, and receive inbound SMS via webhooks or WebSocket."
  },
  "servers": [{ "url": "https://gateway.gravitysms.com" }],
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/health": {
      "get": {
        "summary": "Health check",
        "description": "Check whether the API server is running and responsive. No authentication required.",
        "operationId": "getHealth",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status"],
                  "properties": { "status": { "type": "string", "example": "ok" } }
                }
              }
            }
          }
        }
      }
    },
    "/v1/sms/send": {
      "post": {
        "summary": "Send SMS",
        "description": "Queue an outbound SMS message for delivery.",
        "operationId": "sendSms",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["to", "from", "body"],
                "properties": {
                  "to": {
                    "type": "string",
                    "description": "Recipient phone number in E.164 format (+ followed by 10–15 digits)."
                  },
                  "from": {
                    "type": "string",
                    "description": "Sender phone number in E.164 format. Must be an SMS-enabled number on the tenant's RingCentral account."
                  },
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 1600,
                    "description": "Message text, 1–1600 characters."
                  },
                  "userId": {
                    "type": ["string", "null"],
                    "description": "Optional identifier to associate the message with a user in your system."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["messageId", "status"],
                  "properties": {
                    "messageId": { "type": "string", "example": "msg_a1b2c3d4e5f6" },
                    "status": { "type": "string", "example": "queued" }
                  }
                }
              }
            }
          },
          "400": { "description": "Missing fields, invalid phone number, or body too long" },
          "401": { "description": "Missing or invalid API key" },
          "403": { "description": "Tenant suspended or inactive" },
          "429": { "description": "Too many requests" }
        }
      }
    },
    "/v1/sms/status/{messageId}": {
      "get": {
        "summary": "Get message status",
        "description": "Get the current status and details of a message.",
        "operationId": "getSmsStatus",
        "parameters": [
          {
            "name": "messageId",
            "in": "path",
            "required": true,
            "description": "Message ID in the format msg_ followed by 12 hex characters.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "messageId": { "type": "string" },
                    "status": {
                      "type": "string",
                      "enum": ["queued", "sending", "sent", "failed", "received"]
                    },
                    "rcMessageId": { "type": ["string", "null"] },
                    "error": { "type": ["string", "null"] },
                    "to": { "type": "string" },
                    "from": { "type": "string" },
                    "body": { "type": "string" },
                    "createdAt": { "type": "string", "format": "date-time" },
                    "updatedAt": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid messageId format" },
          "404": { "description": "Message not found" }
        }
      }
    },
    "/v1/sms/messages": {
      "get": {
        "summary": "List messages",
        "description": "Query paginated message history with optional filters.",
        "operationId": "listSmsMessages",
        "parameters": [
          {
            "name": "appId",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter by app ID."
          },
          {
            "name": "tenantId",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter by tenant ID."
          },
          {
            "name": "userId",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter by the userId field set when sending."
          },
          {
            "name": "direction",
            "in": "query",
            "schema": { "type": "string", "enum": ["outbound", "inbound"] },
            "description": "Filter by direction."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["queued", "sending", "sent", "failed", "received"]
            },
            "description": "Filter by status."
          },
          {
            "name": "from",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter by sender phone number."
          },
          {
            "name": "to",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter by recipient phone number."
          },
          {
            "name": "startDate",
            "in": "query",
            "schema": { "type": "string", "format": "date-time" },
            "description": "ISO 8601 timestamp. Messages created on or after this date."
          },
          {
            "name": "endDate",
            "in": "query",
            "schema": { "type": "string", "format": "date-time" },
            "description": "ISO 8601 timestamp. Messages created on or before this date."
          },
          {
            "name": "page",
            "in": "query",
            "schema": { "type": "integer", "default": 1 },
            "description": "Page number, starting at 1."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 20, "maximum": 100 },
            "description": "Results per page."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["messages", "total", "page", "limit"],
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "messageId": { "type": "string" },
                          "jobId": { "type": "string" },
                          "appId": { "type": "string" },
                          "tenantId": { "type": "string" },
                          "userId": { "type": ["string", "null"] },
                          "direction": { "type": "string" },
                          "to": { "type": "string" },
                          "from": { "type": "string" },
                          "body": { "type": "string" },
                          "status": { "type": "string" },
                          "rcMessageId": { "type": ["string", "null"] },
                          "error": { "type": ["string", "null"] },
                          "createdAt": { "type": "string", "format": "date-time" },
                          "updatedAt": { "type": "string", "format": "date-time" }
                        }
                      }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/apps/register": {
      "post": {
        "summary": "Register app",
        "description": "Register a new application and receive an API key.",
        "operationId": "registerApp",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "tenantId"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100,
                    "description": "App display name."
                  },
                  "tenantId": {
                    "type": "string",
                    "description": "Tenant to associate the app with. Format: tenant_ + 16 hex characters."
                  },
                  "webhookUrl": {
                    "type": ["string", "null"],
                    "maxLength": 2000,
                    "description": "URL to receive inbound SMS webhooks. Must be HTTPS in production."
                  },
                  "role": {
                    "type": "string",
                    "enum": ["app"],
                    "description": "Default 'app'. Each key is scoped to your account."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "appId": { "type": "string" },
                    "name": { "type": "string" },
                    "apiKey": {
                      "type": "string",
                      "description": "API key (returned once, store immediately)."
                    },
                    "apiKeyPrefix": { "type": "string" },
                    "webhookSecret": {
                      "type": "string",
                      "description": "Webhook signing secret (whsec_ prefix). Returned once at creation time; use to verify webhook signatures."
                    },
                    "role": { "type": "string" },
                    "tenantId": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid name, tenant, or webhook URL" },
          "403": { "description": "Forbidden" }
        }
      }
    },
    "/v1/apps": {
      "get": {
        "summary": "List apps",
        "description": "List all apps in your account.",
        "operationId": "listApps",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apps": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "appId": { "type": "string" },
                          "tenantId": { "type": "string" },
                          "name": { "type": "string" },
                          "webhookUrl": { "type": ["string", "null"] },
                          "role": { "type": "string" },
                          "isActive": { "type": "boolean" },
                          "apiKeyPrefix": { "type": "string" },
                          "lastUsedAt": { "type": ["string", "null"], "format": "date-time" },
                          "createdAt": { "type": "string", "format": "date-time" },
                          "updatedAt": { "type": "string", "format": "date-time" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/apps/{appId}": {
      "put": {
        "summary": "Update app",
        "description": "Update an app's webhook URL or active status.",
        "operationId": "updateApp",
        "parameters": [
          {
            "name": "appId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "App ID (app_ + 16 hex characters)."
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "webhookUrl": { "type": ["string", "null"], "maxLength": 2000 },
                  "isActive": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } } }
              }
            }
          },
          "400": { "description": "Invalid appId or webhook URL" },
          "404": { "description": "App not found" }
        }
      },
      "delete": {
        "summary": "Delete app",
        "description": "Deactivate an app (soft delete). The API key stops working immediately.",
        "operationId": "deleteApp",
        "parameters": [
          { "name": "appId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } } }
              }
            }
          },
          "400": { "description": "Invalid appId format" },
          "404": { "description": "App not found" }
        }
      }
    },
    "/v1/apps/{appId}/rotate-key": {
      "post": {
        "summary": "Rotate API key",
        "description": "Generate a new API key, immediately invalidating the old one.",
        "operationId": "rotateAppKey",
        "parameters": [
          { "name": "appId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiKey": { "type": "string" },
                    "apiKeyPrefix": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid appId format" },
          "404": { "description": "App not found" }
        }
      }
    },
    "/v1/apps/{appId}/rotate-webhook-secret": {
      "post": {
        "summary": "Rotate webhook secret",
        "description": "Generate a new webhook secret, immediately invalidating the old one. Use the new secret to verify webhook signatures.",
        "operationId": "rotateAppWebhookSecret",
        "parameters": [
          { "name": "appId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhookSecret": {
                      "type": "string",
                      "description": "New webhook signing secret (whsec_ prefix)."
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid appId format" },
          "404": { "description": "App not found" }
        }
      }
    },
    "/v1/apps/{appId}/test-webhook": {
      "post": {
        "summary": "Test webhook",
        "description": "Send a test payload to the app's configured webhook URL. 5 second timeout.",
        "operationId": "testAppWebhook",
        "parameters": [
          { "name": "appId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "OK (ok: true if webhook returned 2xx; ok: false with status/error otherwise)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "status": { "type": "integer" },
                    "error": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid appId or app has no webhookUrl" },
          "404": { "description": "App not found" }
        }
      }
    },
    "/v1/webhooks/ringcentral": {
      "post": {
        "summary": "RingCentral webhook receiver",
        "description": "Called by RingCentral to deliver inbound SMS events. Not called by your application. Documented for reference; configure webhookUrl on your app to receive inbound SMS.",
        "operationId": "webhooksRingCentral",
        "security": [],
        "responses": {
          "200": { "description": "Event accepted" }
        }
      }
    },
    "/v1/rc/connect-token": {
      "post": {
        "summary": "Get connect token",
        "description": "Generate a single-use token to start the RingCentral OAuth flow. Returns connectUrl; prepend base URL and open in browser.",
        "operationId": "rcConnectToken",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "connectUrl": { "type": "string", "example": "/v1/rc/connect?token=abc123..." }
                  }
                }
              }
            }
          },
          "429": { "description": "Too many requests (5 tokens per tenant per hour)" }
        }
      }
    },
    "/v1/rc/connect": {
      "get": {
        "summary": "Start OAuth flow",
        "description": "Open in browser. Validates token and redirects to RingCentral authorization page. No Bearer auth; token in query.",
        "operationId": "rcConnect",
        "security": [],
        "parameters": [
          { "name": "token", "in": "query", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "302": { "description": "Redirect to RingCentral" }
        }
      }
    },
    "/v1/ws/token": {
      "post": {
        "summary": "Generate WebSocket token",
        "description": "Generate a short-lived, single-use token for WebSocket authentication. Use this when connecting from browser environments where exposing the API key is not desirable. The token inherits the same identity (appId, tenantId, role) as the API key used to create it.",
        "operationId": "createWsToken",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["token", "expiresIn"],
                  "properties": {
                    "token": {
                      "type": "string",
                      "example": "wst_a1b2c3d4e5f6789012345678abcdef90a1b2c3d4e5f6789012345678abcdef90",
                      "description": "Ephemeral token prefixed with wst_. Single-use; consumed when used to authenticate a WebSocket connection."
                    },
                    "expiresIn": {
                      "type": "integer",
                      "example": 60,
                      "description": "Token lifetime in seconds. The token cannot be used after this period."
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Missing or invalid API key" },
          "403": { "description": "Tenant suspended or inactive" },
          "429": { "description": "Too many requests" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key",
        "description": "API key in format sgw_... (returned when registering an app or rotating key)"
      }
    }
  }
}
