{
  "openapi": "3.1.0",
  "info": {
    "title": "JS1 Talkback Send API",
    "version": "1",
    "description": "Send approved template messages from your number and hear back what happened to each one. The full guide, with examples and the rules your code must follow, is at https://talkback.js1software.net/docs/api — this file says the same thing for tools such as Postman.",
    "contact": { "name": "JS1 Software support" }
  },
  "servers": [ { "url": "https://talkback.js1software.net" } ],
  "security": [ { "bearer": [] } ],
  "paths": {
    "/v1/messages": {
      "post": {
        "operationId": "sendMessage",
        "summary": "Send a message",
        "description": "JSON body (at most 64 KB), or multipart/form-data with a 'message' field holding the same JSON and a 'document' PDF (for a template with a document header) or an 'image' JPEG/PNG (for a template with an image header), at most 5 MB. Nothing is charged on any refusal.",
        "parameters": [ { "$ref": "#/components/parameters/IdempotencyKey" } ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SendRequest" },
              "example": { "to": "919876543210", "template": "invoice_send", "params": [ "Ramesh Textiles", "INV-1043", "12,500" ], "reference": "INV-1043" }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [ "message" ],
                "properties": {
                  "message": { "type": "string", "description": "The SendRequest JSON, as text." },
                  "document": { "type": "string", "format": "binary", "description": "PDF only, at most 5 MB. The customer sees the file's own name." },
                  "image": { "type": "string", "format": "binary", "description": "JPEG or PNG, at most 5 MB, for a template with an image header." }
                }
              },
              "encoding": { "document": { "contentType": "application/pdf" }, "image": { "contentType": "image/jpeg, image/png" } }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Taken. Save 'id'; webhooks tell you the rest.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendAccepted" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/Refused" },
          "403": { "$ref": "#/components/responses/Refused" },
          "409": { "$ref": "#/components/responses/Refused" },
          "413": { "$ref": "#/components/responses/Refused" },
          "422": { "$ref": "#/components/responses/Refused" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/Refused" }
        }
      }
    },
    "/v1/messages/{id}": {
      "get": {
        "operationId": "getMessage",
        "summary": "One message's status",
        "description": "For a one-off check. Use webhooks to learn what happened to every message; do not poll this.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "maxLength": 50 }, "example": "msg_3f9c0a7e5b1d42c88e0f6a1b2c3d4e5f" }
        ],
        "responses": {
          "200": {
            "description": "The message's status now.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendState" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Refused" },
          "404": { "description": "No such message, not yours, or a test message asked with a live key (or the other way)." },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/balance": {
      "get": {
        "operationId": "getBalance",
        "summary": "Messages left",
        "responses": {
          "200": {
            "description": "The balance.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Balance" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Refused" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/readiness": {
      "get": {
        "operationId": "getReadiness",
        "summary": "Would a real send of these templates be taken now?",
        "description": "Asks without sending. Call it with your live key before switching a customer over. A test key always answers ready: true, test: true.",
        "parameters": [
          { "name": "template", "in": "query", "required": false, "style": "form", "explode": true, "schema": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 100 } } }
        ],
        "responses": {
          "200": {
            "description": "Ready or not, and why.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Readiness" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Refused" },
          "422": { "$ref": "#/components/responses/Refused" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/public/prices": {
      "get": {
        "operationId": "getPublicPrices",
        "summary": "The list price, the same for everyone",
        "description": "No key needed. Rupees before GST. An account with its own agreed price sees it on its Billing page, not here.",
        "security": [],
        "responses": {
          "200": {
            "description": "The price list in force now.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicPrices" } } }
          },
          "503": { "description": "No price list is in force (plain text)." }
        }
      }
    }
  },
  "webhooks": {
    "message.status": {
      "post": {
        "summary": "A message you sent moved (sent, delivered, read, failed)",
        "parameters": [ { "$ref": "#/components/parameters/WebhookTimestamp" }, { "$ref": "#/components/parameters/WebhookSignature" }, { "$ref": "#/components/parameters/WebhookEventId" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageStatusEvent" } } } },
        "responses": { "200": { "description": "Any 2xx within 10 seconds. Anything else is retried for 24 hours." } }
      }
    },
    "message.received": {
      "post": {
        "summary": "A customer wrote to your number",
        "parameters": [ { "$ref": "#/components/parameters/WebhookTimestamp" }, { "$ref": "#/components/parameters/WebhookSignature" }, { "$ref": "#/components/parameters/WebhookEventId" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageReceivedEvent" } } } },
        "responses": { "200": { "description": "Any 2xx within 10 seconds." } }
      }
    },
    "webhook.test": {
      "post": {
        "summary": "Sent when you press Send a test on the Developers page",
        "parameters": [ { "$ref": "#/components/parameters/WebhookTimestamp" }, { "$ref": "#/components/parameters/WebhookSignature" }, { "$ref": "#/components/parameters/WebhookEventId" } ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookTestEvent" } } } },
        "responses": { "200": { "description": "Any 2xx within 10 seconds." } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": { "type": "http", "scheme": "bearer", "description": "tb_live_… (real messages, charged) or tb_test_… (nothing sent, nothing charged). Made on the Developers page." }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key", "in": "header", "required": true,
        "description": "One per message you mean to send. A retry with the same key never sends twice; a new message needs a new key.",
        "schema": { "type": "string", "minLength": 1, "maxLength": 200, "pattern": "^[\\x21-\\x7E]+$" },
        "example": "INV-1043-send-1"
      },
      "WebhookTimestamp": { "name": "Talkback-Timestamp", "in": "header", "required": true, "description": "Unix time in seconds. Reject one more than 5 minutes from your clock.", "schema": { "type": "string" } },
      "WebhookSignature": { "name": "Talkback-Signature", "in": "header", "required": true, "description": "\"v1=\" + lowercase hex HMAC-SHA256 over \"{timestamp}.{raw body}\", keyed with the UTF-8 bytes of the whole tbwh_… secret.", "schema": { "type": "string" } },
      "WebhookEventId": { "name": "Talkback-Event-Id", "in": "header", "required": true, "description": "Same as eventId in the body. Drop a repeat.", "schema": { "type": "string" } }
    },
    "responses": {
      "Unauthorized": { "description": "Missing, wrong or withdrawn key. No body. Final: fix the key." },
      "Refused": {
        "description": "Refused with a stable code. Nothing is charged. Act on 'code'; 'message' is for a person.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "RATE_LIMITED (wait 60 seconds) or, for a test key, TEST_LIMIT_REACHED (tomorrow, IST).",
        "headers": { "Retry-After": { "description": "Seconds to wait (RATE_LIMITED).", "schema": { "type": "integer" } } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "SendRequest": {
        "type": "object",
        "required": [ "to", "template" ],
        "properties": {
          "to": { "type": "string", "description": "With the country code: 11 to 15 digits, not starting with 0. +, spaces and hyphens are ignored.", "example": "919876543210" },
          "template": { "type": "string", "minLength": 1, "maxLength": 100 },
          "params": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 1000 }, "description": "Values for {{1}}, {{2}}, … in order. No new line or tab, not more than 4 spaces in a row." },
          "language": { "type": "string", "description": "e.g. en, en_US, hi. Left out: the one language the template is approved in." },
          "reference": { "type": "string", "maxLength": 64, "description": "Your own label, e.g. the invoice number. Sent back in webhooks." }
        }
      },
      "SendAccepted": {
        "type": "object",
        "required": [ "id", "status", "remaining" ],
        "properties": {
          "id": { "type": "string", "example": "msg_3f9c0a7e5b1d42c88e0f6a1b2c3d4e5f" },
          "status": { "$ref": "#/components/schemas/Status" },
          "remaining": { "type": [ "integer", "null" ], "description": "Messages left; null for a test key." }
        }
      },
      "SendState": {
        "type": "object",
        "required": [ "id", "status", "failureCode", "updated" ],
        "properties": {
          "id": { "type": "string" },
          "status": { "$ref": "#/components/schemas/Status" },
          "failureCode": { "oneOf": [ { "$ref": "#/components/schemas/FailureCode" }, { "type": "null" } ] },
          "updated": { "type": "string", "format": "date-time" }
        }
      },
      "Balance": {
        "type": "object",
        "required": [ "remaining", "expiring", "mode" ],
        "properties": {
          "remaining": { "type": [ "integer", "null" ] },
          "expiring": {
            "type": "array",
            "items": { "type": "object", "required": [ "messages", "on" ], "properties": { "messages": { "type": "integer" }, "on": { "type": "string", "format": "date-time" } } }
          },
          "mode": { "type": "string", "enum": [ "enforce", "shadow", "off" ], "description": "enforce = paid; shadow = free trial, counted not charged; off = sending not switched on." }
        }
      },
      "PublicPrices": {
        "type": "object",
        "required": [ "currency", "gstPercent", "minAmount", "packs", "bestValue", "rates", "effectiveFromUtc" ],
        "properties": {
          "currency": { "type": "string", "examples": [ "INR" ] },
          "gstPercent": { "type": "integer", "description": "Added on top of every amount here." },
          "minAmount": { "type": "integer", "description": "The smallest pack that can be bought, before GST." },
          "packs": { "type": "array", "items": { "type": "integer" }, "description": "The pack amounts on offer, before GST; none is below minAmount." },
          "bestValue": { "type": "integer", "description": "The pack marked best value." },
          "rates": {
            "type": "array",
            "description": "Smallest fromAmount first. A pack pays the perMessage of the highest fromAmount it reaches.",
            "items": { "type": "object", "required": [ "fromAmount", "perMessage" ], "properties": { "fromAmount": { "type": "number" }, "perMessage": { "type": "number" } } }
          },
          "effectiveFromUtc": { "type": "string", "format": "date-time", "description": "When this price list came into force." }
        }
      },
      "Readiness": {
        "type": "object",
        "required": [ "ready", "test", "problems", "templates" ],
        "properties": {
          "ready": { "type": "boolean" },
          "test": { "type": "boolean" },
          "problems": {
            "type": "array",
            "items": { "type": "object", "required": [ "code", "message" ], "properties": { "code": { "$ref": "#/components/schemas/ErrorCode" }, "message": { "type": "string" } } }
          },
          "templates": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [ "name", "approved", "problem" ],
              "properties": { "name": { "type": "string" }, "approved": { "type": "boolean" }, "problem": { "type": [ "string", "null" ], "description": "Starts with the send's own code." } }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [ "code", "message" ],
        "properties": { "code": { "$ref": "#/components/schemas/ErrorCode" }, "message": { "type": "string" } }
      },
      "ErrorCode": {
        "type": "string",
        "description": "Act on this; it never changes. Rarely a code from RejectedCode on a 422 or 503.",
        "enum": [
          "INSUFFICIENT_BALANCE", "SHOP_LIMIT_REACHED",
          "ACCOUNT_OFF", "ACCOUNT_PAUSED", "SHOP_NOT_LIVE", "NUMBER_RESTRICTED",
          "IDEMPOTENCY_KEY_REUSED",
          "DOCUMENT_TOO_LARGE",
          "BAD_PARAMS", "TEMPLATE_NOT_APPROVED", "LANGUAGE_REQUIRED", "MARKETING_NOT_OFFERED", "UNKNOWN_SHOP", "PROVIDER_REFUSED",
          "RATE_LIMITED", "TEST_LIMIT_REACHED",
          "PROVIDER_UNAVAILABLE", "PROVIDER_NOT_READY"
        ]
      },
      "Status": {
        "type": "string",
        "enum": [ "accepted", "submitted", "sent", "delivered", "read", "failed", "rejected" ],
        "description": "failed is final and still counted; rejected was never sent and not charged."
      },
      "FailureCode": {
        "type": "string",
        "description": "On a failed message: NOT_REACHABLE, TEMPLATE_REJECTED, DOCUMENT_FAILED, RATE_LIMITED, UNDELIVERABLE. On a rejected one: a RejectedCode.",
        "enum": [ "NOT_REACHABLE", "TEMPLATE_REJECTED", "DOCUMENT_FAILED", "RATE_LIMITED", "UNDELIVERABLE",
                  "PROVIDER_REFUSED", "PROVIDER_UNAVAILABLE", "PROVIDER_NOT_READY", "TEMPLATE_NOT_APPROVED", "DOCUMENT_EXPIRED", "NOT_SENT" ]
      },
      "RejectedCode": {
        "type": "string",
        "enum": [ "PROVIDER_REFUSED", "PROVIDER_UNAVAILABLE", "PROVIDER_NOT_READY", "TEMPLATE_NOT_APPROVED", "DOCUMENT_EXPIRED", "NOT_SENT" ]
      },
      "MessageStatusEvent": {
        "type": "object",
        "required": [ "event", "eventId", "id", "reference", "status", "failureCode", "at", "test" ],
        "properties": {
          "event": { "const": "message.status" },
          "eventId": { "type": "string" },
          "id": { "type": "string" },
          "reference": { "type": [ "string", "null" ] },
          "status": { "type": "string", "enum": [ "sent", "delivered", "read", "failed" ] },
          "failureCode": { "oneOf": [ { "$ref": "#/components/schemas/FailureCode" }, { "type": "null" } ], "description": "Only on a live failed." },
          "at": { "type": "string", "format": "date-time" },
          "test": { "type": "boolean" }
        }
      },
      "MessageReceivedEvent": {
        "type": "object",
        "required": [ "event", "eventId", "from", "name", "type", "text", "at", "test" ],
        "properties": {
          "event": { "const": "message.received" },
          "eventId": { "type": "string" },
          "from": { "type": "string", "example": "919876543210" },
          "name": { "type": [ "string", "null" ] },
          "type": { "type": "string", "enum": [ "text", "image", "document", "audio", "video", "location", "other" ] },
          "text": { "type": [ "string", "null" ], "maxLength": 3000 },
          "at": { "type": "string", "format": "date-time" },
          "test": { "type": "boolean" }
        }
      },
      "WebhookTestEvent": {
        "type": "object",
        "required": [ "event", "eventId", "at", "test" ],
        "properties": {
          "event": { "const": "webhook.test" },
          "eventId": { "type": "string" },
          "at": { "type": "string", "format": "date-time" },
          "test": { "type": "boolean" }
        }
      }
    }
  }
}
