{
  "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" },
                    "receivingExtensionId": {
                      "type": "string",
                      "description": "RC extension ID that received the inbound message. Present on inbound messages only; omitted on outbound."
                    },
                    "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"] },
                          "receivingExtensionId": {
                            "type": "string",
                            "description": "RC extension ID that received the inbound message. Present on inbound messages only; omitted on outbound."
                          },
                          "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/webhook-deliveries": {
      "get": {
        "summary": "List webhook delivery attempts",
        "description": "List webhook delivery attempts for your tenant (both app webhookUrl fan-outs and trigger callbackUrl fan-outs), sorted newest-first. Tenant-scoped; you only see attempts for your own tenant. Attempts are retained for 30 days. No retries today: each attempt is a single shot, so a failed attempt stays failed. When retries ship, additional rows will share the same deliveryId with incrementing attemptNumber.",
        "operationId": "listWebhookDeliveries",
        "parameters": [
          {
            "name": "appId",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter to deliveries for one app (must belong to your tenant)."
          },
          {
            "name": "triggerId",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter to deliveries for one trigger subscription (must belong to your tenant)."
          },
          {
            "name": "messageId",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Filter to all attempts tied to one inbound message."
          },
          {
            "name": "outcome",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["success", "http_error", "network_error", "timeout"]
            },
            "description": "Filter by delivery outcome."
          },
          {
            "name": "kind",
            "in": "query",
            "schema": { "type": "string", "enum": ["app", "trigger"] },
            "description": "Filter by destination kind: 'app' (fan-out to webhookUrl) or 'trigger' (fan-out to a trigger's callbackUrl)."
          },
          {
            "name": "startDate",
            "in": "query",
            "schema": { "type": "string", "format": "date-time" },
            "description": "ISO 8601 timestamp. Attempts on or after this time."
          },
          {
            "name": "endDate",
            "in": "query",
            "schema": { "type": "string", "format": "date-time" },
            "description": "ISO 8601 timestamp. Attempts on or before this time."
          },
          {
            "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": ["attempts", "total", "page", "limit"],
                  "properties": {
                    "attempts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "tenantId",
                          "kind",
                          "messageId",
                          "url",
                          "outcome",
                          "durationMs",
                          "attemptedAt",
                          "deliveryId",
                          "attemptNumber"
                        ],
                        "properties": {
                          "tenantId": {
                            "type": "string",
                            "description": "Your tenant id."
                          },
                          "kind": {
                            "type": "string",
                            "enum": ["app", "trigger"],
                            "description": "'app' (fan-out to an app's webhookUrl) or 'trigger' (fan-out to a trigger subscription's callbackUrl)."
                          },
                          "appId": {
                            "type": "string",
                            "description": "The app whose webhookUrl was the destination. Present when kind=app."
                          },
                          "triggerId": {
                            "type": "string",
                            "description": "The trigger subscription that owned the destination. Present when kind=trigger."
                          },
                          "messageId": {
                            "type": "string",
                            "description": "The inbound SMS that triggered this fan-out."
                          },
                          "url": {
                            "type": "string",
                            "description": "The destination URL we POSTed to."
                          },
                          "outcome": {
                            "type": "string",
                            "enum": ["success", "http_error", "network_error", "timeout"],
                            "description": "success: 2xx response. http_error: non-2xx response. timeout: no response within 5s. network_error: could not reach endpoint (DNS, refused, TLS, etc.)."
                          },
                          "statusCode": {
                            "type": "integer",
                            "description": "HTTP status code. Present whenever we got a response (success or http_error)."
                          },
                          "errorClass": {
                            "type": "string",
                            "description": "JavaScript error name (e.g. AbortError, TypeError). Present on network_error and timeout."
                          },
                          "errorMessage": {
                            "type": "string",
                            "description": "Short error message, truncated to 500 chars."
                          },
                          "responseSnippet": {
                            "type": "string",
                            "description": "First 256 bytes of the response body. Stored on http_error only (not on success) to keep payloads small."
                          },
                          "durationMs": {
                            "type": "integer",
                            "description": "Round-trip time in milliseconds (monotonic clock)."
                          },
                          "attemptedAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Wall-clock timestamp when the attempt was made."
                          },
                          "deliveryId": {
                            "type": "string",
                            "description": "Stable id for the logical delivery (<messageId>:<appId or triggerId>). Spans multiple attempts when retries ship."
                          },
                          "attemptNumber": {
                            "type": "integer",
                            "description": "Always 1 today; will be 1..N when retries ship."
                          },
                          "correlationId": {
                            "type": "string",
                            "description": "Ties this attempt back to the inbound RingCentral webhook event. One inbound event can fan out to N destinations — they share this id."
                          }
                        }
                      }
                    },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid filter (outcome, kind, startDate, endDate, or messageId)"
          },
          "401": { "description": "Missing or invalid API key" },
          "403": { "description": "Tenant suspended or inactive" },
          "429": { "description": "Too many requests" }
        }
      }
    },
    "/v1/rc/extensions": {
      "get": {
        "summary": "List connected extensions",
        "description": "List all connected RingCentral extensions for the tenant. Each connected extension's phone numbers are available as from addresses when sending SMS.",
        "operationId": "rcListExtensions",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "extensions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "extensionId": { "type": "string", "example": "12345678" },
                          "rcAccountId": { "type": "string", "example": "000000000" },
                          "connectedAt": { "type": "string", "format": "date-time" },
                          "status": {
                            "type": "string",
                            "enum": ["active", "refresh_failed", "revoked"],
                            "description": "active: ready to send; refresh_failed: temporary issue; revoked: re-authorization required."
                          },
                          "phoneNumbers": {
                            "type": "array",
                            "items": { "type": "string" },
                            "description": "SMS-capable phone numbers (E.164) owned by this extension. Use these as the from address when sending SMS for this user.",
                            "example": ["+15551234567", "+15557654321"]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Missing or invalid API key" },
          "403": { "description": "Tenant suspended or inactive" }
        }
      }
    },
    "/v1/rc/invite-token": {
      "post": {
        "summary": "Generate extension invite token",
        "description": "Generate a single-use invite link for an additional RC extension user to authorize. Once they authorize, your app can immediately send SMS from their RC number(s). Rate limited to 5 tokens per tenant per hour (shared with /v1/rc/connect-token).",
        "operationId": "rcInviteToken",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "connectUrl": { "type": "string", "example": "/v1/rc/connect?token=abc123..." }
                  }
                }
              }
            }
          },
          "401": { "description": "Missing or invalid API key" },
          "403": { "description": "Tenant suspended or inactive" },
          "429": { "description": "Too many requests (5 tokens per tenant per hour)" }
        }
      }
    },
    "/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)"
      }
    }
  }
}
