{
  "openapi": "3.1.0",
  "info": {
    "title": "Velroi Messages API",
    "version": "1.0.0",
    "description": "Server-to-server text messaging through a Mac. A successful submission is not a delivery confirmation. Client must verify current opt-in for every recipient."
  },
  "servers": [
    {
      "url": "https://imessage.velroi.com"
    },
    {
      "url": "http://127.0.0.1:3001",
      "description": "Local Mac origin"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/v1/messages": {
      "post": {
        "summary": "Submit or dry-run one text message",
        "description": "One live operation at a time; 30 live attempts and 120 authenticated requests per minute. Same key and payload replay the original result, including uncertain failures. Persist keys across retries. Unknown request outcomes are never automatically resent. No attachments or batch sends.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Required for live sends; optional for dryRun=true. Stable unique key per logical message.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9._:-]{8,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageRequest"
              },
              "examples": {
                "dryRun": {
                  "value": {
                    "to": "+12025550123",
                    "text": "Your appointment is confirmed.",
                    "consent": true,
                    "dryRun": true
                  }
                },
                "sms": {
                  "value": {
                    "to": "+12025550123",
                    "text": "Your appointment is confirmed.",
                    "service": "SMS",
                    "consent": true
                  }
                },
                "group": {
                  "value": {
                    "chatId": "PASTE_EXACT_ID_FROM_GROUPS",
                    "text": "The meeting starts in 15 minutes.",
                    "consent": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dry run validated; nothing sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DryRun"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "202": {
            "description": "Submitted to Messages; not delivery confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Submission"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing consent/key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Group or path not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflicting idempotency key or unconfirmed previous outcome",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "Body exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Busy or rate limit; follow Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "502": {
            "description": "Submission outcome uncertain; do not issue a new key blindly",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Messages unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "sendMessage"
      }
    },
    "/send": {
      "post": {
        "summary": "Alias of POST /v1/messages",
        "description": "One live operation at a time; 30 live attempts and 120 authenticated requests per minute. Same key and payload replay the original result, including uncertain failures. Persist keys across retries. Unknown request outcomes are never automatically resent. No attachments or batch sends.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Required for live sends; optional for dryRun=true. Stable unique key per logical message.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9._:-]{8,128}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageRequest"
              },
              "examples": {
                "dryRun": {
                  "value": {
                    "to": "+12025550123",
                    "text": "Your appointment is confirmed.",
                    "consent": true,
                    "dryRun": true
                  }
                },
                "sms": {
                  "value": {
                    "to": "+12025550123",
                    "text": "Your appointment is confirmed.",
                    "service": "SMS",
                    "consent": true
                  }
                },
                "group": {
                  "value": {
                    "chatId": "PASTE_EXACT_ID_FROM_GROUPS",
                    "text": "The meeting starts in 15 minutes.",
                    "consent": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dry run validated; nothing sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DryRun"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "202": {
            "description": "Submitted to Messages; not delivery confirmation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Submission"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request or missing consent/key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Group or path not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "Conflicting idempotency key or unconfirmed previous outcome",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "Body exceeds 64 KiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "415": {
            "description": "Use application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "Busy or rate limit; follow Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "502": {
            "description": "Submission outcome uncertain; do not issue a new key blindly",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "Messages unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Idempotency-Replayed": {
                "description": "true on a replay of a completed request",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait on HTTP 429",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "sendMessageAlias"
      }
    },
    "/health": {
      "get": {
        "operationId": "health",
        "security": [],
        "summary": "Process liveness only; does not test Messages",
        "responses": {
          "200": {
            "description": "Running",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "const": "ok"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/groups": {
      "get": {
        "operationId": "listGroups",
        "summary": "List existing group IDs and names",
        "responses": {
          "200": {
            "description": "Groups",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "groups": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Group"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Messages unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/capabilities": {
      "get": {
        "operationId": "capabilities",
        "summary": "Supported features and limits",
        "responses": {
          "200": {
            "description": "Feature flags, limits, and consent/submission semantics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/requests/{requestId}": {
      "get": {
        "operationId": "requestStatus",
        "summary": "Stored submission outcome; not a delivery receipt",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Known request",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requestId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "sending",
                        "submitted",
                        "unknown"
                      ]
                    },
                    "result": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Submission"
                        },
                        {
                          "$ref": "#/components/schemas/Error"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Group or path not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      },
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    },
    "schemas": {
      "MessageRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "text",
          "consent"
        ],
        "oneOf": [
          {
            "required": [
              "to"
            ],
            "not": {
              "required": [
                "chatId"
              ]
            }
          },
          {
            "required": [
              "chatId"
            ],
            "not": {
              "required": [
                "to"
              ]
            }
          }
        ],
        "properties": {
          "to": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "E.164 phone, email, or exact existing group ID."
          },
          "chatId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "Existing group chat ID from GET /v1/groups; mutually exclusive with to."
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000,
            "description": "Nonblank text; NUL is rejected."
          },
          "consent": {
            "type": "boolean",
            "const": true,
            "description": "Client attests current opt-in for every recipient, including all group members."
          },
          "service": {
            "type": "string",
            "enum": [
              "iMessage",
              "SMS",
              "auto"
            ],
            "description": "Defaults to iMessage for direct recipients and auto for groups. Groups only accept auto; SMS requires a phone. Auto uses Messages default participant routing without retry/fallback."
          },
          "dryRun": {
            "type": "boolean",
            "default": false
          },
          "group": {
            "type": "boolean",
            "description": "Optional compatibility flag. Modern group IDs are detected automatically."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "requestId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          }
        }
      },
      "Submission": {
        "type": "object",
        "required": [
          "requestId",
          "status",
          "service",
          "submittedAt"
        ],
        "properties": {
          "requestId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "const": "submitted"
          },
          "service": {
            "type": "string",
            "enum": [
              "iMessage",
              "SMS",
              "auto",
              "conversation"
            ]
          },
          "submittedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DryRun": {
        "type": "object",
        "properties": {
          "status": {
            "const": "validated"
          },
          "dryRun": {
            "const": true
          },
          "to": {
            "type": "string"
          },
          "service": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Group": {
        "type": "object",
        "required": [
          "chatId",
          "displayName"
        ],
        "properties": {
          "chatId": {
            "type": "string"
          },
          "displayName": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    }
  }
}
