{
  "openapi": "3.1.0",
  "info": {
    "title": "canhelpto public API",
    "version": "1.0.0",
    "description": "Customer support for MCP servers and web apps. Tickets, replies, feedback and CSAT for one workspace, authenticated with the workspace key in X-API-Key. Agent docs: https://canhelpto.com/docs/agents",
    "contact": {
      "email": "support@canhelpto.com"
    }
  },
  "servers": [
    {
      "url": "https://canhelpto.com"
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Either workspace key. The public API key (the widget id, data-app-id) opens what the widget can do; service mode (X-Service-Mode, mode=service, PATCH, internal notes) needs the service key (Dashboard -> Settings -> Service key) once the workspace has one; before that the public key still opens it."
      }
    },
    "parameters": {
      "ServiceMode": {
        "name": "X-Service-Mode",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        },
        "description": "Send 'true' when a server (an MCP wrapper, a backend) acts on behalf of a customer. Tickets are then marked source API; on reads, internal notes are included, so leave it out when the result goes back to the customer."
      }
    },
    "schemas": {
      "NewTicket": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string",
            "description": "The customer's words. A server may append a context block (account, client, last calls with errors) under them."
          },
          "summary": {
            "type": "string",
            "description": "One line for the inbox. When given, AI triage is skipped."
          },
          "customerName": {
            "type": "string"
          },
          "customerEmail": {
            "type": "string",
            "format": "email",
            "description": "Where the team's reply is sent. Without it the reply is readable only through the API."
          },
          "customerPhone": {
            "type": "string"
          },
          "priority": {
            "type": "string",
            "enum": [
              "LOW",
              "MEDIUM",
              "HIGH",
              "URGENT"
            ],
            "default": "MEDIUM"
          },
          "category": {
            "type": "string",
            "description": "Free text; the MCP tools use bug | question | billing | account | feature."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "For example [\"mcp\", \"claude-connector\"]."
          },
          "page": {
            "type": "string",
            "description": "Where the ticket came from, for example mcp://gigi or https://app.example.com/billing."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Your own data, returned on read. The MCP wrapper stores {mcp: {account, client, recentCalls}}; use metadata.mcp.account.userId to tell a customer's tickets apart under the workspace key."
          },
          "images": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "object",
              "required": [
                "filename",
                "contentType",
                "data"
              ],
              "properties": {
                "filename": {
                  "type": "string"
                },
                "contentType": {
                  "type": "string",
                  "enum": [
                    "image/png",
                    "image/jpeg",
                    "image/gif",
                    "image/webp"
                  ]
                },
                "data": {
                  "type": "string",
                  "description": "base64, 2MB max"
                }
              }
            }
          }
        }
      },
      "TicketCreated": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "ticket": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "priority": {
                "type": "string"
              },
              "source": {
                "type": "string",
                "enum": [
                  "WIDGET",
                  "API",
                  "EMAIL",
                  "WHATSAPP"
                ]
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "Reply": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "isAI": {
            "type": "boolean"
          },
          "isInternal": {
            "type": "boolean",
            "description": "Only present with X-Service-Mode; never show an internal note to the customer."
          },
          "userId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when a team member wrote it; null for the customer."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Ticket": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "OPEN",
              "IN_PROGRESS",
              "RESOLVED",
              "CLOSED"
            ]
          },
          "priority": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "FEEDBACK",
              "BUG",
              "FEATURE_REQUEST",
              "QUESTION",
              "OTHER"
            ]
          },
          "source": {
            "type": "string"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "message": {
            "type": "string"
          },
          "customerName": {
            "type": [
              "string",
              "null"
            ]
          },
          "customerEmail": {
            "type": [
              "string",
              "null"
            ]
          },
          "sentiment": {
            "type": [
              "string",
              "null"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "page": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "replies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Reply"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/tickets": {
      "post": {
        "summary": "Open a ticket",
        "operationId": "createTicket",
        "parameters": [
          {
            "$ref": "#/components/parameters/ServiceMode"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewTicket"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Ticket opened; AI triage runs in the background unless summary was given.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TicketCreated"
                }
              }
            }
          },
          "400": {
            "description": "message missing or an image invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key"
          }
        }
      },
      "get": {
        "summary": "List a customer's tickets",
        "operationId": "listTickets",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Customer email; required unless mode=service."
          },
          {
            "name": "phone",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "priority",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "service"
              ]
            },
            "description": "Every ticket of the workspace, internal notes included: for the vendor's own tooling only."
          }
        ],
        "responses": {
          "200": {
            "description": "Tickets, newest first, with their replies.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tickets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Ticket"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "email or phone required"
          },
          "401": {
            "description": "Invalid API key"
          }
        }
      }
    },
    "/api/v1/tickets/{ticketId}": {
      "get": {
        "summary": "Read a ticket with its public replies",
        "operationId": "getTicket",
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/ServiceMode"
          }
        ],
        "responses": {
          "200": {
            "description": "The ticket. Check metadata.mcp.account.userId or customerEmail against the caller before showing it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          },
          "404": {
            "description": "Not in this workspace"
          }
        }
      },
      "patch": {
        "summary": "Update status, priority, category or assignee (service mode only)",
        "operationId": "updateTicket",
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/ServiceMode"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "OPEN",
                      "IN_PROGRESS",
                      "RESOLVED",
                      "CLOSED"
                    ]
                  },
                  "priority": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "assignedToId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          },
          "403": {
            "description": "X-Service-Mode: true required"
          },
          "404": {
            "description": "Not in this workspace"
          }
        }
      }
    },
    "/api/v1/tickets/{ticketId}/replies": {
      "post": {
        "summary": "The customer (or, in service mode, the vendor) writes on the ticket",
        "operationId": "replyToTicket",
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/ServiceMode"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Required without service mode; must match the ticket's customerEmail."
                  },
                  "message": {
                    "type": "string"
                  },
                  "isInternal": {
                    "type": "boolean",
                    "description": "Service mode only; default true there."
                  },
                  "images": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Reply stored; a resolved ticket reopens on a customer reply."
          },
          "403": {
            "description": "Email does not match the ticket"
          },
          "404": {
            "description": "Not in this workspace"
          }
        }
      }
    },
    "/api/v1/tickets/{ticketId}/status": {
      "patch": {
        "summary": "The customer closes or reopens their own ticket",
        "operationId": "setTicketStatus",
        "parameters": [
          {
            "name": "ticketId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "status"
                ],
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated"
          }
        }
      }
    },
    "/api/v1/tickets/csat": {
      "get": {
        "summary": "Rate a resolved ticket 1-5 (the link in the resolution email)",
        "operationId": "rateTicket",
        "security": [],
        "parameters": [
          {
            "name": "ticketId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rating",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recorded"
          }
        }
      }
    },
    "/api/v1/feedback": {
      "post": {
        "summary": "Quick feedback (GOOD / NEUTRAL / BAD with a comment)",
        "operationId": "sendFeedback",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rating"
                ],
                "properties": {
                  "rating": {
                    "type": "string",
                    "enum": [
                      "GOOD",
                      "NEUTRAL",
                      "BAD"
                    ]
                  },
                  "comment": {
                    "type": "string"
                  },
                  "page": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored"
          },
          "400": {
            "description": "Invalid rating"
          }
        }
      }
    },
    "/api/v1/widget-config": {
      "get": {
        "summary": "The workspace's widget configuration (what the web widget loads)",
        "operationId": "getWidgetConfig",
        "responses": {
          "200": {
            "description": "Config JSON"
          }
        }
      }
    }
  }
}
