{
  "openapi": "3.1.0",
  "info": {
    "title": "ONPAD API",
    "version": "1.0.0",
    "summary": "Send WhatsApp messages from your own software.",
    "description": "The ONPAD API queues WhatsApp messages for delivery through the official WhatsApp Business Cloud API.\n\nSending is asynchronous: POST /messages validates the request and returns 202 with an id, then a background worker delivers the message and retries up to 3 times with backoff. Poll GET /messages to see the outcome.\n\nWhatsApp's own rules still apply. Free-form text is only allowed inside the 24-hour customer service window; outside it you must send a template Meta has approved.",
    "contact": {
      "name": "ONPAD support",
      "email": "support@onpad.in",
      "url": "https://onpad.in/docs/api"
    }
  },
  "servers": [
    {
      "url": "https://app.onpad.in/api/v1",
      "description": "Production. There is no sandbox — keys are live and messages are delivered and charged."
    }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Messages", "description": "Send a message and check what happened to it." },
    { "name": "Workspace", "description": "Verify a key and read the connection behind it." }
  ],
  "paths": {
    "/messages": {
      "post": {
        "tags": ["Messages"],
        "operationId": "sendMessage",
        "summary": "Queue a message for delivery",
        "description": "Validates the request and queues the message. Returns 202 immediately — delivery happens in the background.\n\nAn Idempotency-Key header is REQUIRED. Repeating a request with the same key and the same body returns the original message with duplicate=true and sends nothing. The same key with a different body returns 409.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "A key that identifies this send, so a retry cannot deliver twice. Derive it from your own data (for example order-10482-shipped) rather than generating a new random value per attempt.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 120,
              "pattern": "^[A-Za-z0-9._:-]{8,120}$"
            },
            "example": "order-10482-shipped"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SendMessageRequest" },
              "examples": {
                "template": {
                  "summary": "Template message (the usual case)",
                  "value": {
                    "to": "919876543210",
                    "type": "template",
                    "template": {
                      "name": "order_shipped",
                      "language": "en_US",
                      "parameters": ["Priya", "#10482", "Friday"]
                    }
                  }
                },
                "text": {
                  "summary": "Free text — only inside an open 24-hour window",
                  "value": {
                    "to": "919876543210",
                    "type": "text",
                    "text": { "body": "Your order is out for delivery and will arrive by 6pm." }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued for delivery.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MessageEnvelope" }
              }
            }
          },
          "200": {
            "description": "A duplicate of an earlier identical request. Nothing was sent again.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MessageEnvelope" }
              }
            }
          },
          "400": {
            "description": "The request body is not valid JSON.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "ok": false, "error": "invalid_json", "message": "Request body must be valid JSON." }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "The workspace is read-only, so sending is paused. Reading still works.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "ok": false,
                  "error": "workspace_read_only",
                  "message": "Your trial has ended. This workspace is read-only. Choose a plan to resume sending and editing."
                }
              }
            }
          },
          "405": {
            "description": "Only GET and POST are accepted on this path.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "ok": false, "error": "method_not_allowed", "message": "Method not allowed." }
              }
            }
          },
          "409": {
            "description": "This Idempotency-Key was already used for a different request body. Nothing was sent.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "ok": false,
                  "error": "message_rejected",
                  "message": "This Idempotency-Key was already used for a different request."
                }
              }
            }
          },
          "422": {
            "description": "The request was understood but something in it is invalid. The message field names the problem. Do not retry without changing the request.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "missingIdempotencyKey": {
                    "summary": "No Idempotency-Key header",
                    "value": { "ok": false, "error": "message_rejected", "message": "Provide an Idempotency-Key header between 8 and 120 safe characters." }
                  },
                  "badRecipient": {
                    "summary": "Recipient missing its country code",
                    "value": { "ok": false, "error": "message_rejected", "message": "Enter a valid recipient phone number with country code." }
                  },
                  "windowClosed": {
                    "summary": "Free text outside the 24-hour window",
                    "value": { "ok": false, "error": "message_rejected", "message": "The 24-hour customer service window is closed. Send an approved template instead." }
                  },
                  "templateNotApproved": {
                    "summary": "Template not approved, or not in that language",
                    "value": { "ok": false, "error": "message_rejected", "message": "This template is not approved or is unavailable in the selected language." }
                  },
                  "parameterCount": {
                    "summary": "Wrong number of template parameters",
                    "value": { "ok": false, "error": "message_rejected", "message": "This template requires exactly 2 parameter(s)." }
                  },
                  "mediaHeader": {
                    "summary": "Template has a media header",
                    "value": { "ok": false, "error": "message_rejected", "message": "Media-header templates are not supported by this endpoint yet." }
                  },
                  "notConnected": {
                    "summary": "No WhatsApp number connected",
                    "value": { "ok": false, "error": "message_rejected", "message": "Connect WhatsApp before syncing message templates." }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Our side failed and the message was NOT queued. This is the only status worth retrying — retry with the same Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "ok": false, "error": "internal_error", "message": "Message could not be queued." }
              }
            }
          }
        }
      },
      "get": {
        "tags": ["Messages"],
        "operationId": "getMessage",
        "summary": "Look up a message",
        "description": "Returns the current state of a message sent through the API.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The id returned when the message was queued.",
            "schema": { "type": "string", "format": "uuid" },
            "example": "6f1b9a2c-4d7e-4a31-9f08-2b5c7d1e3a40"
          }
        ],
        "responses": {
          "200": {
            "description": "The message.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "message"],
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "message": { "$ref": "#/components/schemas/Message" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "No message with that id in this workspace. An id belonging to another workspace also returns 404.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "ok": false, "error": "message_not_found", "message": "Message not found." }
              }
            }
          },
          "422": {
            "description": "The id parameter was not provided.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "ok": false, "error": "missing_message_id", "message": "Provide a message ID." }
              }
            }
          }
        }
      }
    },
    "/status": {
      "get": {
        "tags": ["Workspace"],
        "operationId": "getStatus",
        "summary": "Check the key, the workspace and the WhatsApp connection",
        "description": "The cheapest way to confirm an integration is configured correctly. Sends nothing and costs nothing.",
        "responses": {
          "200": {
            "description": "The key is valid.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Status" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An ONPAD API key, created under Settings → API keys. Keys begin with wh_live_ and are shown in full only once. API access is a plan feature: a workspace whose plan excludes it receives 401 for every request."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The key is missing, malformed, revoked, or the workspace's plan does not include API access. All four return the same response.",
        "headers": {
          "WWW-Authenticate": {
            "schema": { "type": "string" },
            "example": "Bearer realm=\"ONPAD API\""
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "ok": false, "error": "invalid_api_key", "message": "Provide a valid active API key." }
          }
        }
      }
    },
    "schemas": {
      "SendMessageRequest": {
        "type": "object",
        "required": ["to"],
        "properties": {
          "to": {
            "type": "string",
            "description": "Recipient in full international form. Separators are stripped; what remains must be 8 to 15 digits. A number without a country code is rejected.",
            "pattern": "^[0-9+\\-() ]{8,25}$",
            "examples": ["919876543210", "+91 98765 43210"]
          },
          "type": {
            "type": "string",
            "enum": ["template", "text"],
            "default": "template",
            "description": "template works at any time. text is only allowed inside an open 24-hour customer service window."
          },
          "template": { "$ref": "#/components/schemas/TemplatePayload" },
          "text": {
            "type": "object",
            "properties": {
              "body": {
                "type": "string",
                "minLength": 1,
                "maxLength": 4096,
                "description": "The message. Required when type is text."
              }
            }
          },
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096,
            "description": "Accepted as a shorthand for text.body."
          }
        }
      },
      "TemplatePayload": {
        "type": "object",
        "required": ["name"],
        "description": "Required when type is template.",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 512,
            "pattern": "^[a-z0-9_]+$",
            "description": "The approved template's name. Lowercase letters, digits and underscores only.",
            "example": "order_shipped"
          },
          "language": {
            "type": "string",
            "default": "en_US",
            "pattern": "^[a-z]{2,3}(?:_[A-Z]{2})?$",
            "description": "Must match the language the template was approved in.",
            "examples": ["en_US", "hi", "pt_BR"]
          },
          "parameters": {
            "type": "array",
            "maxItems": 20,
            "description": "Values for {{1}}, {{2}} … in order. The count must equal the number of distinct placeholders in the approved body.",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 1024
            },
            "example": ["Priya", "#10482", "Friday"]
          }
        }
      },
      "MessageEnvelope": {
        "type": "object",
        "required": ["ok", "message"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "duplicate": {
            "type": "boolean",
            "description": "true when this Idempotency-Key had already been used for an identical request, in which case nothing was sent."
          },
          "message": { "$ref": "#/components/schemas/Message" }
        }
      },
      "Message": {
        "type": "object",
        "required": ["id", "to", "type", "status"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The ONPAD message id."
          },
          "to": { "type": "string", "description": "The recipient, normalised with a leading +.", "example": "+919876543210" },
          "type": { "type": "string", "enum": ["template", "text"] },
          "status": {
            "type": "string",
            "enum": ["queued", "processing", "sent", "delivered", "read", "failed"],
            "description": "read only appears when the recipient has read receipts switched on, so its absence means nothing."
          },
          "meta_message_id": {
            "type": ["string", "null"],
            "description": "Meta's own message id, available once sent."
          },
          "attempts": { "type": "integer", "description": "Delivery attempts made so far. Maximum 3." },
          "error": { "type": ["string", "null"], "description": "Why it failed, when status is failed." },
          "created_at": { "type": "string", "format": "date-time" },
          "sent_at": { "type": ["string", "null"], "format": "date-time" },
          "delivered_at": { "type": ["string", "null"], "format": "date-time" },
          "read_at": { "type": ["string", "null"], "format": "date-time" },
          "status_url": {
            "type": "string",
            "format": "uri",
            "description": "Present on a send. A convenience URL for looking this message up."
          }
        }
      },
      "Status": {
        "type": "object",
        "required": ["ok", "workspace", "whatsapp", "server_time"],
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "workspace": {
            "type": "object",
            "properties": {
              "id": { "type": "integer" },
              "name": { "type": "string" },
              "slug": { "type": "string" },
              "country": { "type": "string" },
              "timezone": { "type": "string", "example": "Asia/Kolkata" }
            }
          },
          "whatsapp": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": ["connected", "not_started", "pending", "error"],
                "description": "Anything other than connected means sends will fail."
              },
              "phone_number": { "type": "string", "description": "Digits only." },
              "verified_name": { "type": "string", "description": "The display name Meta approved." }
            }
          },
          "server_time": { "type": "string", "format": "date-time" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["ok", "error", "message"],
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": {
            "type": "string",
            "description": "A stable code to branch on.",
            "enum": [
              "invalid_json",
              "invalid_api_key",
              "workspace_read_only",
              "method_not_allowed",
              "message_rejected",
              "message_not_found",
              "missing_message_id",
              "internal_error"
            ]
          },
          "message": {
            "type": "string",
            "description": "A human-readable explanation. Wording may change — do not match on it in code."
          }
        }
      }
    }
  }
}
