{
  "openapi": "3.1.0",
  "info": {
    "title": "Maandrik Voz",
    "version": "0.2.0",
    "description": "One outbound text-to-speech call per request. PostgreSQL, Azure Speech Ryan pt-PT and Asterisk ARI with Zadarma SIP. Alarm orchestration is external. Menus and inbound calling are not implemented."
  },
  "servers": [
    {
      "url": "http://127.0.0.1:8088",
      "description": "Local development. Calls are simulated by default."
    }
  ],
  "security": [
    {
      "CampaignKey": []
    }
  ],
  "paths": {
    "/calls": {
      "post": {
        "operationId": "createCall",
        "summary": "Create a call",
        "description": "Persists and accepts a single outbound call. No automatic redial, scheduling or escalation. The campaign comes from the API key. Text is synthesized asynchronously; poll GET /calls/{id} for the result. Menus are rejected.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCall"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Call accepted; does not prove the recipient answered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Call"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input or unsupported menu.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Idempotency-Key already used with different content.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "All call lines are in use. No call was accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Calling service is unavailable. No call was accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128,
              "pattern": "^[!-~]+$"
            },
            "description": "Use the same key and content to safely resend a request. Scoped to the campaign, retained with call history. Reusing the key with different content returns 409."
          }
        ]
      }
    },
    "/calls/{id}": {
      "get": {
        "operationId": "getCall",
        "summary": "Get call status",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Call belonging to the authenticated campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Call"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Call not found in this campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "health",
        "summary": "Check process health",
        "description": "Checks PostgreSQL and controller readiness. Does not verify Speech, SIP registration, telephone delivery or audible playback.",
        "security": [],
        "responses": {
          "200": {
            "description": "Process is available.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": ["ok"],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Controller or database unavailable."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "CampaignKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Opaque API key scoped to a campaign. Store only its hash."
      }
    },
    "schemas": {
      "CreateCall": {
        "type": "object",
        "additionalProperties": false,
        "required": ["to", "text"],
        "properties": {
          "to": {
            "type": "string",
            "description": "Portuguese landline/mobile. National 9-digit numbers are normalized to +351; spaces, parentheses and hyphens are accepted.",
            "example": "+351900000000"
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "pattern": "\\S",
            "description": "Plain text to read in European Portuguese.",
            "example": "Alarme Maandrik. O servidor de aplicações está indisponível."
          },
          "menu": {
            "type": "null",
            "description": "Only absent or null is supported. Any menu object returns 400."
          }
        }
      },
      "Call": {
        "type": "object",
        "required": [
          "id",
          "to",
          "status",
          "project",
          "menu",
          "response",
          "direction",
          "answeredAt",
          "durationSeconds",
          "estimatedCost",
          "createdAt",
          "updatedAt",
          "mode"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "to": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "preparing",
              "calling",
              "answered",
              "reading",
              "completed",
              "not_answered",
              "hung_up_early",
              "failed",
              "unknown"
            ],
            "description": "completed proves playback finished, not that a human understood or acknowledged the message. unknown means an interrupted or ambiguous execution; consult the operator before requesting a new call. simulated calls do not reach a telephone."
          },
          "reason": {
            "type": ["string", "null"]
          },
          "project": {
            "type": ["string", "null"]
          },
          "menu": {
            "type": "null"
          },
          "response": {
            "type": "null"
          },
          "direction": {
            "type": "string",
            "const": "outbound"
          },
          "answeredAt": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "durationSeconds": {
            "type": ["integer", "null"],
            "minimum": 0
          },
          "estimatedCost": {
            "type": ["number", "null"],
            "minimum": 0,
            "description": "Estimated EUR cost; null when no reliable tariff or usage is available. Never substitute Azure prices for another provider."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "mode": {
            "type": "string",
            "enum": ["live", "simulated"]
          }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "English message explaining what happened."
          }
        }
      }
    }
  }
}
