{
  "openapi": "3.1.0",
  "info": {
    "title": "Wally",
    "version": "1.0.0",
    "summary": "Send SMS, MMS and email to people who agreed to hear from you.",
    "description": "Pay as you go messaging. Consent, opt-outs and quiet hours are enforced by the platform, not by the caller: a request to message somebody who opted out is refused with 403 recipient_opted_out no matter which key made it. Costs are drawn from a credit balance and every message is billed at the moment a carrier accepts it.",
    "contact": { "email": "support@wallysolutions.com", "url": "https://wallysolutions.com" }
  },
  "servers": [ { "url": "https://api.wallysolutions.com/v1" } ],
  "security": [ { "bearerAuth": [] } ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from the Developers page of the Wally console. Send it as: Authorization: Bearer wly_live_..."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string", "description": "Stable machine readable reason." },
              "message": { "type": "string", "description": "Sentence intended for a human." }
            }
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "description": "Use this to check status later." },
          "status": { "type": "string", "enum": ["queued","sending","sent","delivered","failed","cancelled"] },
          "to": { "type": "string" },
          "channel": { "type": "string", "enum": ["sms","mms","email"] },
          "segments": { "type": "integer", "description": "SMS is billed per segment. 160 GSM characters per segment, or 70 if the text contains any non-GSM character." },
          "estimated_cost": { "type": "number" },
          "unicode": { "type": "boolean", "description": "True when the body forced UCS-2, which roughly doubles the cost." }
        }
      },
      "Contact": {
        "type": "object",
        "properties": {
          "contact_id": { "type": "integer" },
          "first_name": { "type": "string" },
          "last_name": { "type": "string" },
          "phone_e164": { "type": "string" },
          "email": { "type": "string" },
          "sms_ok": { "type": "boolean", "description": "Consent on record for text messages." },
          "email_ok": { "type": "boolean" },
          "suppressed": { "type": "boolean", "description": "Opted out. Cannot be messaged on any channel they opted out of." }
        }
      }
    }
  },
  "paths": {
    "/messages": {
      "post": {
        "operationId": "sendMessage",
        "summary": "Send one message to one person",
        "description": "Queues a message. Returns 202 because sending happens within about a minute, not during this request. Refused with 403 if the recipient has opted out or has no consent on record, and with 402 if the account has insufficient credit. Supply an Idempotency-Key header and a retry returns the first response rather than sending twice.",
        "security": [ { "bearerAuth": [] } ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string", "maxLength": 120 },
            "description": "Any unique string you generate. Strongly recommended for anything automated: a timeout followed by a retry would otherwise send a second message that the recipient receives and you pay for."
          }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["to","body"],
            "properties": {
              "to": { "type": "string", "description": "US mobile number in any readable format, or an email address when channel is email." },
              "body": { "type": "string", "description": "The message. Keep it under 160 GSM characters to stay in one segment." },
              "channel": { "type": "string", "enum": ["sms","mms","email"], "default": "sms" },
              "subject": { "type": "string", "description": "Email only." },
              "media_url": { "type": "string", "description": "MMS only. Use a media: marker from the Wally console rather than a public URL." }
            }
          } } }
        },
        "responses": {
          "202": { "description": "Queued.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } },
          "402": { "description": "Not enough credit.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Refused on purpose: recipient opted out, no consent on record, or the account is not approved for sending.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "That Idempotency-Key was used with a different body." },
          "429": { "description": "Rate limited. Retry-After says how long to wait." }
        }
      },
      "get": {
        "operationId": "getMessage",
        "summary": "Check what happened to a message",
        "parameters": [ { "name": "id", "in": "query", "required": true, "schema": { "type": "integer" } } ],
        "responses": {
          "200": { "description": "Current state.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } },
          "404": { "description": "No message with that id on this account." }
        }
      }
    },
    "/contacts": {
      "get": {
        "operationId": "listContacts",
        "summary": "Search your contacts",
        "parameters": [
          { "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Name, email or number." },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 200 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } }
        ],
        "responses": { "200": { "description": "A page of contacts.", "content": { "application/json": { "schema": {
          "type": "object",
          "properties": {
            "data": { "type": "array", "items": { "$ref": "#/components/schemas/Contact" } },
            "total": { "type": "integer" },
            "limit": { "type": "integer" },
            "offset": { "type": "integer" }
          }
        } } } } }
      },
      "post": {
        "operationId": "saveContact",
        "summary": "Create or update a contact",
        "description": "consent_note is required and is stored as evidence against the contact. Describe how this person actually agreed to be messaged. Adding somebody who has opted out does not resubscribe them: the contact is created with consent off.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["consent_note"],
            "properties": {
              "id": { "type": "integer", "description": "Omit to create." },
              "first_name": { "type": "string" },
              "last_name": { "type": "string" },
              "phone": { "type": "string" },
              "email": { "type": "string" },
              "tags": { "type": "string", "description": "Comma separated." },
              "external_ref": { "type": "string", "description": "Your own identifier for this person." },
              "list_id": { "type": "integer" },
              "consent_note": { "type": "string", "description": "How they agreed. For example: ticked the box on our booking form on 3 March." }
            }
          } } }
        },
        "responses": {
          "201": { "description": "Created." },
          "200": { "description": "Updated." },
          "400": { "description": "Missing consent_note, or an invalid number." }
        }
      }
    }
  }
}