{
  "openapi": "3.1.0",
  "info": {
    "title": "SMSBlast API",
    "version": "2.0.0",
    "summary": "Send SMS and MMS and sync opt-outs with your organization's API key.",
    "description": "The SMSBlast API is self-serve: create a free account at https://app.smsblast.io/signup, then get or rotate your organization API key in the app under Settings → Integrations (APIs). Keep the key secret; anyone with it can send messages billed to your account, and rotating it immediately invalidates the old one.\n\nAuthenticate every request with `Authorization: Bearer YOUR_API_KEY`; requests without it get HTTP 401.\n\nErrors return a non-2xx status with a JSON body: `{ \"error\": \"...\", \"details\": \"...\" }`, where `error` says what went wrong and the optional `details` says how to fix it. All endpoints are rate limited (HTTP 429).\n\nPhone numbers are E.164 (`+` country code + number). Messaging is pay as you go from your prepaid balance; current rates: https://smsblast.io/pricing.",
    "termsOfService": "https://smsblast.io/terms"
  },
  "externalDocs": {
    "description": "SMSBlast developer portal",
    "url": "https://smsblast.io/developers"
  },
  "servers": [
    {
      "url": "https://app.smsblast.io",
      "description": "SMSBlast application (production)"
    }
  ],
  "security": [{ "bearerApiKey": [] }],
  "tags": [
    { "name": "Account", "description": "Test your key and read account details." },
    { "name": "Messaging", "description": "Send SMS and MMS." },
    { "name": "Contacts", "description": "Keep opt-outs in sync." }
  ],
  "paths": {
    "/api/v2/me": {
      "get": {
        "operationId": "getAccount",
        "tags": ["Account"],
        "summary": "Test your API key and read account details",
        "description": "Confirms the key works and returns your organization id, name, prepaid balance, default sender number, and its verification status. Read-only and free; use it as the connection test in no-code tools.",
        "responses": {
          "200": {
            "description": "The key is valid.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Account" },
                "examples": {
                  "default": { "value": { "id": "org_uuid", "name": "Acme Inc", "balance": 42.5, "sender_number": "+18885551234", "sender_status": "verified" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v2/sms/send": {
      "post": {
        "operationId": "sendSms",
        "tags": ["Messaging"],
        "summary": "Send an SMS or MMS to one or many recipients",
        "description": "Sends `message` (and/or the image at `mediaUrl`, as MMS) from a verified number you own (`from`) to one recipient or an array of up to 100. Recipients who have opted out are skipped and reported under `errors` with code `OPTED_OUT`. Each sent message is billed per segment (MMS per message) against your prepaid balance; the request is rejected with 402 when the balance cannot cover the whole send. Accepts JSON, `application/x-www-form-urlencoded`, or `multipart/form-data` bodies.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SendSmsRequest" },
              "examples": {
                "singleRecipient": {
                  "summary": "One recipient, with a contact name",
                  "value": { "from": "+18885551234", "to": "+15551234567", "message": "Hello from SMSBlast!", "name": "Jane Doe" }
                },
                "multipleRecipients": {
                  "summary": "Up to 100 recipients",
                  "value": { "from": "+18885551234", "to": ["+15551234567", "+15559876543"], "message": "Hi!" }
                },
                "mms": {
                  "summary": "MMS with an image",
                  "value": { "from": "+18885551234", "to": "+15551234567", "message": "Check this out!", "mediaUrl": "https://example.com/flyer.jpg" }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": { "$ref": "#/components/schemas/SendSmsRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The send was processed. Check `sent`, `failed`, and `errors` for per-recipient outcomes.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SendResult" },
                "examples": {
                  "default": {
                    "value": {
                      "success": true,
                      "sent": 1,
                      "failed": 0,
                      "totalCost": "0.0150",
                      "results": [{ "to": "+15551234567", "status": "sent", "messageSid": "SM...", "segments": 1, "cost": 0.015 }]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid `from`, the sender is not verified or is reply-only, no `message` or `mediaUrl`, invalid `mediaUrl`, missing `to`, or more than 100 recipients.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "missingFrom": { "value": { "error": "Missing \"from\" field", "details": "v2 requires a \"from\" phone number. Pass the phone number you want to send from." } },
                  "notVerified": { "value": { "error": "Phone number not verified", "details": "The \"from\" number must be verified before it can send SMS." } },
                  "tooManyRecipients": { "value": { "error": "Maximum 100 recipients per request" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientBalance" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/v2/contacts/opt-out": {
      "post": {
        "operationId": "optOutContact",
        "tags": ["Contacts"],
        "summary": "Opt a contact out",
        "description": "Marks the contact as opted out so they no longer receive messages. The contact is matched by phone number (last 10 digits) within your organization. Idempotent: an already opted-out contact returns `alreadyOptedOut: true` with `optedOut: 0`. Recorded as a manual opt-out, which does not count against your sending reputation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/OptOutRequest" },
              "examples": { "default": { "value": { "number": "+15551234567" } } }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The contact is opted out (or already was).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/OptOutResult" },
                "examples": {
                  "newlyOptedOut": { "value": { "success": true, "optedOut": 1, "alreadyOptedOut": false, "number": "+15551234567" } },
                  "alreadyOptedOut": { "value": { "success": true, "optedOut": 0, "alreadyOptedOut": true, "number": "+15551234567" } }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid `number`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "invalidNumber": { "value": { "error": "Missing or invalid \"number\"", "details": "Provide a phone number, e.g. \"+15551234567\"." } } } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "No contact in your account matches that number.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "notFound": { "value": { "error": "Contact not found", "details": "No contact in your account matches that phone number." } } } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    }
  },
  "webhooks": {
    "inboundMessage": {
      "post": {
        "operationId": "inboundMessageWebhook",
        "summary": "Inbound message delivered to your listening webhook",
        "description": "Configure a listening webhook in the app under Settings → Integrations (APIs) → Webhooks. SMSBlast POSTs this payload to your URL whenever a contact texts one of your numbers. Opt-out keywords such as STOP and UNSUBSCRIBE are not forwarded. `from` is the contact's number and `to` is your SMSBlast number that received the text.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InboundMessageEvent" },
              "examples": {
                "default": {
                  "value": {
                    "from": "+15551234567",
                    "to": "+18005551234",
                    "message": "Hi, can you tell me more?",
                    "contact": {
                      "firstName": "Jane",
                      "lastName": "Doe",
                      "phone": "+15551234567",
                      "email": "jane@example.com",
                      "address": "123 Main St",
                      "city": "Austin",
                      "state": "TX",
                      "zip": "78701",
                      "customFields": { "interestedIn": "kitchen remodel", "referralSource": "instagram" }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Return any 2xx status to acknowledge the event." }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key",
        "description": "Your organization API key from Settings → Integrations (APIs), sent as `Authorization: Bearer YOUR_API_KEY` on every request."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": {
              "missing": { "value": { "error": "Missing API key", "details": "Provide \"Authorization: Bearer YOUR_API_KEY\" header." } },
              "invalid": { "value": { "error": "Invalid API key" } }
            }
          }
        }
      },
      "InsufficientBalance": {
        "description": "Your prepaid balance cannot cover this request. `details` states the required and available amounts. Top up in the app under Billing.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "examples": { "default": { "value": { "error": "Insufficient balance", "details": "Required: <amount>, Available: <amount>" } } }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Slow down and retry.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "default": { "value": { "error": "Internal server error" } } } } }
      }
    },
    "schemas": {
      "PhoneNumber": {
        "type": "string",
        "description": "E.164 phone number: `+`, country code, number.",
        "pattern": "^\\+?[1-9]\\d{1,14}$",
        "examples": ["+15551234567"]
      },
      "Account": {
        "type": "object",
        "required": ["id", "name", "balance", "sender_number", "sender_status"],
        "properties": {
          "id": { "type": "string", "format": "uuid", "description": "Organization id." },
          "name": { "type": "string", "description": "Organization name." },
          "balance": { "type": "number", "description": "Current prepaid balance in USD." },
          "sender_number": { "type": ["string", "null"], "description": "Default sender number, E.164, or null if none." },
          "sender_status": { "type": ["string", "null"], "description": "Verification status of the default sender number, for example `verified`." }
        }
      },
      "SendSmsRequest": {
        "type": "object",
        "required": ["from", "to"],
        "description": "Provide `message`, `mediaUrl`, or both.",
        "properties": {
          "from": {
            "allOf": [{ "$ref": "#/components/schemas/PhoneNumber" }],
            "description": "A verified sender number your organization owns, E.164. Reply-only numbers cannot send."
          },
          "to": {
            "description": "One recipient, or an array of up to 100 recipients, E.164. A comma-separated string is also accepted.",
            "oneOf": [
              { "$ref": "#/components/schemas/PhoneNumber" },
              { "type": "array", "items": { "$ref": "#/components/schemas/PhoneNumber" }, "minItems": 1, "maxItems": 100 }
            ]
          },
          "message": {
            "type": "string",
            "description": "Message text. Optional only when `mediaUrl` is present (image-only MMS). Supports placeholders such as {{firstName}}, {{lastName}}, {{email}}, {{address}}, {{city}}, {{state}}, {{zip}}, and your own custom fields, filled per recipient from their contact record (empty fields render blank). For compliance, \"Reply STOP to unsubscribe\" is appended automatically unless the text already contains opt-out wording or is a one-time passcode."
          },
          "name": {
            "type": "string",
            "description": "Optional. Saves this name on the recipient's contact (first word as first name, the rest as last name). Applied only when `to` is a single recipient; ignored for multi-recipient sends."
          },
          "mediaUrl": {
            "type": "string",
            "format": "uri",
            "description": "Optional. A publicly reachable https image URL (jpg, png, gif, or webp up to 5 MB). Sends the message as MMS to every recipient, billed at the MMS rate."
          }
        }
      },
      "SendResult": {
        "type": "object",
        "required": ["success", "sent", "failed", "totalCost", "results"],
        "properties": {
          "success": { "type": "boolean", "description": "True when the request was processed (individual recipients may still fail; see `errors`)." },
          "sent": { "type": "integer", "description": "Number of recipients the message was sent to." },
          "failed": { "type": "integer", "description": "Number of recipients that could not be sent." },
          "totalCost": { "type": "string", "description": "Total cost of the sent messages in USD, as a decimal string, billed against your prepaid balance." },
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/SendResultItem" } },
          "errors": {
            "type": "array",
            "description": "Present only when some recipients could not be sent.",
            "items": { "$ref": "#/components/schemas/SendError" }
          }
        }
      },
      "SendResultItem": {
        "type": "object",
        "required": ["to", "status", "messageSid", "segments", "cost"],
        "properties": {
          "to": { "$ref": "#/components/schemas/PhoneNumber" },
          "status": { "type": "string", "description": "`sent` when the carrier accepted the message.", "examples": ["sent"] },
          "messageSid": { "type": "string", "description": "Carrier message id for this send." },
          "segments": { "type": "integer", "description": "SMS segments billed (1 for MMS)." },
          "cost": { "type": "number", "description": "Cost of this message in USD." }
        }
      },
      "SendError": {
        "type": "object",
        "required": ["to", "error"],
        "properties": {
          "to": { "type": "string", "description": "The recipient as you sent it." },
          "error": { "type": "string", "description": "Why it was not sent, for example an invalid phone number format or an opted-out recipient." },
          "code": { "type": "string", "description": "Machine-readable reason when available.", "enum": ["OPTED_OUT", "BLOCKED_NUMBER"] }
        }
      },
      "OptOutRequest": {
        "type": "object",
        "required": ["number"],
        "properties": {
          "number": { "allOf": [{ "$ref": "#/components/schemas/PhoneNumber" }], "description": "The contact's phone number; matched by its last 10 digits." }
        }
      },
      "OptOutResult": {
        "type": "object",
        "required": ["success", "optedOut", "alreadyOptedOut", "number"],
        "properties": {
          "success": { "type": "boolean" },
          "optedOut": { "type": "integer", "description": "How many matching contacts were newly opted out." },
          "alreadyOptedOut": { "type": "boolean", "description": "True when every matching contact was already opted out." },
          "number": { "type": "string", "description": "The number as you sent it." }
        }
      },
      "InboundMessageEvent": {
        "type": "object",
        "required": ["from", "to", "message", "contact"],
        "properties": {
          "from": { "allOf": [{ "$ref": "#/components/schemas/PhoneNumber" }], "description": "The contact's number." },
          "to": { "allOf": [{ "$ref": "#/components/schemas/PhoneNumber" }], "description": "Your SMSBlast number that received the text." },
          "message": { "type": "string", "description": "The text the contact sent." },
          "contact": { "$ref": "#/components/schemas/WebhookContact" }
        }
      },
      "WebhookContact": {
        "type": "object",
        "description": "What is on file for the sender. Fields are null when not set; new (previously unknown) numbers arrive with mostly null fields.",
        "required": ["firstName", "lastName", "phone", "email", "address", "city", "state", "zip", "customFields"],
        "properties": {
          "firstName": { "type": ["string", "null"] },
          "lastName": { "type": ["string", "null"] },
          "phone": { "type": ["string", "null"] },
          "email": { "type": ["string", "null"] },
          "address": { "type": ["string", "null"] },
          "city": { "type": ["string", "null"] },
          "state": { "type": ["string", "null"] },
          "zip": { "type": ["string", "null"] },
          "customFields": {
            "type": "object",
            "description": "Your custom fields for this contact; `{}` when the organization has none.",
            "additionalProperties": { "type": ["string", "null"] }
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Structured error response.",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "What went wrong. Safe to show to a person.", "examples": ["Invalid API key"] },
          "details": { "type": "string", "description": "How to fix it, when available." }
        }
      }
    }
  }
}
