{
  "openapi": "3.1.0",
  "info": {
    "title": "Kusha SMS API",
    "version": "4.0.0",
    "summary": "Send SMS to Nepali mobile numbers (NTC, Ncell, Smart Telecom).",
    "description": "Token-authenticated REST API for the Kusha SMS platform.\n\nIMPORTANT for automated clients:\n\n1. Every endpoint answers HTTP 200, including for authentication failures, validation failures and an empty wallet. Decide success from the `error` field in the body, never from the status code.\n2. A send can partially succeed: `data.valid` lists queued (charged) messages and `data.invalid` lists aborted (uncharged) ones.\n3. There is no sandbox. Every accepted message is delivered to a real handset and spends real credit. Check `GET /sms/v4/available-credit` first and send test messages only to numbers you control.\n4. Prefer the v4 endpoints; v3 and v1 exist for compatibility with older integrations.\n\nThe human manual is at https://kushasms.com/api-docs and the same content as Markdown at https://kushasms.com/llms-full.txt.",
    "contact": {"name": "Kusha SMS", "url": "https://kushasms.com/api-docs", "email": "info@sahasratech.com.np"}
  },
  "servers": [{"url": "https://kushasms.com"}],
  "tags": [
    {"name": "Send", "description": "Dispatch messages"},
    {"name": "Credit", "description": "Wallet balance"},
    {"name": "Reports", "description": "Delivery history"}
  ],
  "security": [{"authToken": []}],
  "paths": {
    "/sms/v4/send-user": {
      "post": {
        "tags": ["Send"],
        "operationId": "sendV4",
        "summary": "Send SMS (v4, arrays)",
        "description": "One `text` broadcasts to every number in `to`; N texts pair index-for-index with N numbers. Send a unique `Idempotency-Key` header to make retries safe — a repeat with the same key returns the original response instead of sending again.",
        "security": [{"authToken": []}],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique per business event. Makes a retried send safe.",
            "schema": {"type": "string", "maxLength": 128},
            "example": "order-4821-otp"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["to", "text"],
                "properties": {
                  "to": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Nepali mobile numbers. 98XXXXXXXX, 97798XXXXXXXX and +977 98-XXXXXXX are all accepted.",
                    "examples": [["98XXXXXXXX", "98YYYYYYYY"]]
                  },
                  "text": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "One entry broadcasts to all recipients; N entries pair with N numbers.",
                    "examples": [["Hello from Kusha SMS"]]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Always 200 — inspect the body.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {"$ref": "#/components/schemas/V4SendSuccess"},
                    {"$ref": "#/components/schemas/V4Failure"},
                    {"$ref": "#/components/schemas/FlatError"}
                  ]
                },
                "examples": {
                  "queued": {
                    "summary": "Accepted and queued",
                    "value": {
                      "responses": [{
                        "error": false,
                        "message": "1 messages have been queued for delivery.",
                        "data": {
                          "valid": [{
                            "id": "12_3456",
                            "mobile": "97798XXXXXXXX",
                            "text": "Hello from Kusha SMS",
                            "credit": 1,
                            "network": "ntc",
                            "status": "queued",
                            "shortcode": "KUSHA"
                          }],
                          "invalid": []
                        }
                      }],
                      "errors": []
                    }
                  },
                  "badNumber": {
                    "summary": "Recipient aborted, nothing charged",
                    "value": {
                      "error": true,
                      "message": "All messages encountered errors.",
                      "errors": [{
                        "error": true,
                        "message": "No valid recipients.",
                        "data": {
                          "valid": [],
                          "invalid": [{"mobile": "12345", "text": "Hello", "credit": 0, "network": "N/A", "status": "aborted", "shortcode": ""}]
                        }
                      }]
                    }
                  },
                  "badToken": {
                    "summary": "Authentication failed",
                    "value": {"error": true, "message": "The provided Auth Token is not valid.", "data": []}
                  }
                }
              }
            }
          }
        }
      }
    },
    "/sms/v3/send": {
      "post": {
        "tags": ["Send"],
        "operationId": "sendV3",
        "summary": "Send SMS (v3, one text to a list)",
        "description": "Legacy shape: a single text to a comma-separated list of numbers. Authenticates with the `auth_token` parameter. `GET` with the same parameters in the query string also works.",
        "security": [{"authTokenParam": []}],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": ["auth_token", "to", "text"],
                "properties": {
                  "auth_token": {"type": "string", "description": "Your API token."},
                  "to": {"type": "string", "description": "Comma-separated Nepali mobile numbers.", "examples": ["98XXXXXXXX,98YYYYYYYY"]},
                  "text": {"type": "string", "examples": ["Hello from Kusha SMS"]}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Always 200 — inspect the body.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {"$ref": "#/components/schemas/V3SendSuccess"},
                    {"$ref": "#/components/schemas/FlatError"}
                  ]
                },
                "examples": {
                  "queued": {
                    "value": {
                      "error": false,
                      "message": "1 messages has been queued for delivery.",
                      "data": {
                        "valid": [{"id": 3456, "mobile": "97798XXXXXXXX", "text": "Hello", "credit": 1, "network": "ncell", "status": "queued"}],
                        "invalid": []
                      }
                    }
                  },
                  "noBalance": {
                    "value": {"error": true, "message": "Not enough balance.", "data": []}
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": ["Send"],
        "operationId": "sendV3Get",
        "summary": "Send SMS (v3, query string)",
        "description": "Identical to the POST form, with the parameters in the query string.",
        "security": [{"authTokenParam": []}],
        "parameters": [
          {"name": "auth_token", "in": "query", "required": true, "schema": {"type": "string"}},
          {"name": "to", "in": "query", "required": true, "schema": {"type": "string"}, "example": "98XXXXXXXX"},
          {"name": "text", "in": "query", "required": true, "schema": {"type": "string"}, "example": "Hello"}
        ],
        "responses": {
          "200": {
            "description": "Always 200 — inspect the body.",
            "content": {"application/json": {"schema": {"oneOf": [
              {"$ref": "#/components/schemas/V3SendSuccess"},
              {"$ref": "#/components/schemas/FlatError"}
            ]}}}
          }
        }
      }
    },
    "/sms/v4/available-credit": {
      "get": {
        "tags": ["Credit"],
        "operationId": "availableCreditV4",
        "summary": "Spendable credit",
        "description": "The cheapest pre-flight check, and the one to size batches against. For an account supplied by a reseller this is the smaller of your own allowance and your provider's remaining credit, so it can fall without you sending anything.",
        "security": [{"authToken": []}],
        "responses": {
          "200": {
            "description": "Always 200 — inspect the body.",
            "content": {"application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "available_credit": {"type": "number", "description": "Credits you can spend right now."},
                  "response_code": {"type": "integer", "const": 200}
                }
              },
              "example": {"available_credit": 750.0, "response_code": 200}
            }}
          }
        }
      }
    },
    "/sms/v4/credit": {
      "post": {
        "tags": ["Credit"],
        "operationId": "creditV4",
        "summary": "Credit summary",
        "security": [{"authToken": []}],
        "responses": {
          "200": {
            "description": "Always 200 — inspect the body.",
            "content": {"application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "available_credit": {"type": "number"},
                  "total_sms_sent": {"type": "integer", "description": "Messages ever sent through the API."},
                  "last_transaction_date": {"type": ["string", "null"], "description": "Y-m-d H:i:s of the most recent API message."},
                  "last_transaction_date_sms_sent": {"type": "integer"},
                  "response_code": {"type": "integer", "const": 202}
                }
              },
              "example": {"available_credit": 750.0, "total_sms_sent": 1240, "last_transaction_date": "2026-08-07 14:05:11", "last_transaction_date_sms_sent": 0, "response_code": 202}
            }}
          }
        }
      }
    },
    "/sms/v1/credit": {
      "post": {
        "tags": ["Credit"],
        "operationId": "creditV1",
        "summary": "Credit summary (v1)",
        "security": [{"authTokenParam": []}],
        "requestBody": {
          "required": true,
          "content": {"application/x-www-form-urlencoded": {"schema": {
            "type": "object",
            "required": ["auth_token"],
            "properties": {"auth_token": {"type": "string"}}
          }}}
        },
        "responses": {
          "200": {
            "description": "Always 200 — inspect the body.",
            "content": {"application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "available_credit": {"type": "number"},
                  "total_sms_sent": {"type": "integer"},
                  "response_code": {"type": "integer", "const": 200}
                }
              },
              "example": {"available_credit": 750.0, "total_sms_sent": 1240, "response_code": 200}
            }}
          }
        }
      }
    },
    "/sms/v4/api-report": {
      "post": {
        "tags": ["Reports"],
        "operationId": "reportV4",
        "summary": "Delivery report",
        "description": "Up to 500 rows, newest first. `credits` is the NET charged total — a refunded message (failed, cancelled) counts zero, so the figure reconciles with the wallet.",
        "security": [{"authToken": []}],
        "requestBody": {
          "required": false,
          "content": {"application/json": {"schema": {
            "type": "object",
            "properties": {
              "start_date": {"type": "string", "format": "date", "examples": ["2026-08-01"]},
              "end_date": {"type": "string", "format": "date", "examples": ["2026-08-07"]},
              "network": {"type": "string", "enum": ["ntc", "ncell", "smarttel"]},
              "mobile": {"type": "string", "description": "Substring match on the recipient."}
            }
          }}}
        },
        "responses": {
          "200": {
            "description": "Always 200 — inspect the body.",
            "content": {"application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "error": {"type": "boolean"},
                  "total": {"type": "integer"},
                  "credits": {"type": "number"},
                  "data": {"type": "array", "items": {"$ref": "#/components/schemas/ReportRow"}},
                  "response_code": {"type": "integer", "const": 200}
                }
              }
            }}
          }
        }
      }
    },
    "/sms/v1/report/api": {
      "post": {
        "tags": ["Reports"],
        "operationId": "reportV1",
        "summary": "Paged delivery log (v1)",
        "description": "50 rows per page, newest first.",
        "security": [{"authTokenParam": []}],
        "requestBody": {
          "required": true,
          "content": {"application/x-www-form-urlencoded": {"schema": {
            "type": "object",
            "required": ["auth_token"],
            "properties": {
              "auth_token": {"type": "string"},
              "page": {"type": "integer", "minimum": 1, "default": 1}
            }
          }}}
        },
        "responses": {
          "200": {
            "description": "Always 200 — inspect the body.",
            "content": {"application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "error": {"type": "boolean"},
                  "page": {"type": "integer"},
                  "per_page": {"type": "integer", "const": 50},
                  "total": {"type": "integer"},
                  "data": {"type": "array", "items": {"$ref": "#/components/schemas/ReportRow"}}
                }
              }
            }}
          }
        }
      }
    }
  },
  "webhooks": {
    "deliveryReceipt": {
      "post": {
        "operationId": "deliveryReceipt",
        "summary": "Delivery receipt callback",
        "description": "POSTed to the URL configured under Developers → Webhooks whenever a message reaches a terminal status. Verify `X-Kusha-Signature` as HMAC-SHA256 of the RAW request body using your webhook secret, compared in constant time. Retried up to 5 times; answer 2xx quickly.",
        "parameters": [
          {"name": "X-Kusha-Event", "in": "header", "schema": {"type": "string", "enum": ["dlr", "inbound", "batch"]}},
          {"name": "X-Kusha-Signature", "in": "header", "schema": {"type": "string"}, "description": "Hex HMAC-SHA256 of the raw body."}
        ],
        "requestBody": {
          "content": {"application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "message_id": {"type": "integer"},
                "to": {"type": "string"},
                "network": {"type": "string"},
                "status": {"$ref": "#/components/schemas/MessageStatus"},
                "smpp_message_id": {"type": "string"},
                "delivered_at": {"type": ["string", "null"], "format": "date-time"}
              }
            },
            "example": {"message_id": 3456, "to": "98XXXXXXXX", "network": "ntc", "status": "delivered", "smpp_message_id": "0f1a2b", "delivered_at": "2026-08-07T14:05:11+05:45"}
          }}
        },
        "responses": {"2xx": {"description": "Acknowledged. Anything else is retried."}}
      }
    }
  },
  "components": {
    "securitySchemes": {
      "authToken": {
        "type": "apiKey",
        "in": "header",
        "name": "auth-token",
        "description": "Your API token, created in the portal under Developers → API Tokens."
      },
      "authTokenParam": {
        "type": "apiKey",
        "in": "query",
        "name": "auth_token",
        "description": "v3/v1 only. May also be sent as a form field."
      }
    },
    "schemas": {
      "MessageStatus": {
        "type": "string",
        "enum": ["queued", "submitted", "sent", "delivered", "failed", "cancelled", "aborted"],
        "description": "queued/submitted/sent/delivered are charged; failed (never reached an operator) and cancelled are refunded; aborted was never accepted and never charged. submitted is terminal for billing — an operator that takes a message and later reports it undelivered leaves it submitted, and charged."
      },
      "ValidEntryV4": {
        "type": "object",
        "description": "One accepted message. Charged on acceptance.",
        "properties": {
          "id": {"type": "string", "description": "\"<tenant>_<message>\".", "examples": ["12_3456"]},
          "mobile": {"type": "string", "description": "Recipient with the 977 country code."},
          "text": {"type": "string"},
          "credit": {"type": "number", "description": "Credits charged — one per segment."},
          "network": {"type": "string", "examples": ["ntc", "ncell", "smarttel", "N/A"]},
          "status": {"type": "string", "const": "queued"},
          "shortcode": {"type": "string", "description": "Sender ID the message goes out as."}
        }
      },
      "ValidEntryV3": {
        "type": "object",
        "properties": {
          "id": {"type": "integer"},
          "mobile": {"type": "string"},
          "text": {"type": "string"},
          "credit": {"type": "number"},
          "network": {"type": "string"},
          "status": {"type": "string", "const": "queued"}
        }
      },
      "InvalidEntry": {
        "type": "object",
        "description": "A recipient that was never accepted — unparseable number, empty text, or a number on your blocklist. Not charged.",
        "properties": {
          "mobile": {"type": "string"},
          "text": {"type": "string"},
          "credit": {"type": "number", "const": 0},
          "network": {"type": "string", "const": "N/A"},
          "status": {"type": "string", "const": "aborted"},
          "shortcode": {"type": "string", "description": "v4 only; empty."}
        }
      },
      "V4SendSuccess": {
        "type": "object",
        "properties": {
          "responses": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "error": {"type": "boolean", "const": false},
                "message": {"type": "string"},
                "data": {
                  "type": "object",
                  "properties": {
                    "valid": {"type": "array", "items": {"$ref": "#/components/schemas/ValidEntryV4"}},
                    "invalid": {"type": "array", "items": {"$ref": "#/components/schemas/InvalidEntry"}}
                  }
                }
              }
            }
          },
          "errors": {"type": "array", "items": {"type": "object"}}
        }
      },
      "V4Failure": {
        "type": "object",
        "description": "No recipient survived validation, or the wallet could not fund the whole batch. Nothing was charged.",
        "properties": {
          "error": {"type": "boolean", "const": true},
          "message": {"type": "string", "const": "All messages encountered errors."},
          "errors": {"type": "array", "items": {"type": "object"}}
        }
      },
      "V3SendSuccess": {
        "type": "object",
        "properties": {
          "error": {"type": "boolean", "const": false},
          "message": {"type": "string"},
          "data": {
            "type": "object",
            "properties": {
              "valid": {"type": "array", "items": {"$ref": "#/components/schemas/ValidEntryV3"}},
              "invalid": {"type": "array", "items": {"$ref": "#/components/schemas/InvalidEntry"}}
            }
          }
        }
      },
      "FlatError": {
        "type": "object",
        "description": "Missing or invalid token, a missing field, an empty wallet, or the rate limit.",
        "properties": {
          "error": {"type": "boolean", "const": true},
          "message": {
            "type": "string",
            "examples": [
              "The auth token field is required.",
              "The provided Auth Token is not valid.",
              "The to field is required.",
              "The text field is required.",
              "Not enough balance.",
              "Rate limit exceeded. Try again shortly."
            ]
          },
          "data": {"type": "array", "items": {}}
        }
      },
      "ReportRow": {
        "type": "object",
        "properties": {
          "id": {"type": "integer"},
          "mobile": {"type": "string"},
          "text": {"type": "string"},
          "credit": {"type": "number"},
          "network": {"type": "string"},
          "status": {"$ref": "#/components/schemas/MessageStatus"},
          "sent_on": {"type": "string", "description": "Y-m-d H:i:s."}
        }
      }
    }
  },
  "x-rate-limit": {
    "requestsPerMinute": 60,
    "scope": "per token, fixed one-minute window",
    "note": "Your plan may set a different figure. Over the limit the body is {\"error\": true, \"message\": \"Rate limit exceeded. Try again shortly.\", \"data\": []} with HTTP 200. Batch recipients into one v4 call rather than one call per number."
  },
  "x-credits": {
    "rule": "One credit per SMS segment.",
    "gsm7": {"single": 160, "concatenated": 153},
    "ucs2": {"single": 70, "concatenated": 67, "note": "Any character outside GSM-7 (Nepali Devanagari, emoji) switches the whole message to UCS-2."}
  }
}
