{
  "openapi": "3.1.0",
  "info": {
    "title": "Bonnard API",
    "version": "1.0.0",
    "description": "Public API surface for bonnard.ai. Currently one endpoint: submitting a contact or demo request on a user's behalf, with their consent. Versioning policy: the API is versioned in the URL path (currently v1). /api/contact is a permanent unversioned alias that always points to the latest version, kept working indefinitely for existing integrations; new integrations should target the versioned path (/api/v1/contact) directly for a stable contract. When a breaking v2 ships, v1 stays available for at least 6 months, and every v1 response in that window carries a `Deprecation: true` header and a `Sunset` header naming the retirement date (RFC 8594). No version is currently deprecated.",
    "contact": {
      "name": "Bonnard",
      "url": "https://bonnard.ai/get-in-touch"
    }
  },
  "servers": [
    {
      "url": "https://bonnard.ai"
    }
  ],
  "security": [],
  "externalDocs": {
    "description": "Bonnard product and developer documentation, including OAuth 2.1 with PKCE for the authenticated MCP surface at app.bonnard.ai. That surface is not part of this specification.",
    "url": "https://docs.bonnard.dev"
  },
  "paths": {
    "/api/v1/contact": {
      "post": {
        "operationId": "submitContactRequest",
        "summary": "Submit a contact or demo request",
        "description": "Submit a contact, demo, or pricing enquiry to the Bonnard team on a user's behalf, with their consent. On success the team is notified and the requester receives a confirmation email. /api/contact (unversioned) is a permanent alias for this same operation. No authentication is required; abuse is controlled by rate limiting, not by credentials.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Request accepted; notification and confirmation emails sent.",
            "headers": {
              "API-Version": {
                "schema": {
                  "type": "string",
                  "example": "1"
                },
                "description": "The API version that served this response."
              },
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer",
                  "example": 5
                },
                "description": "Requests allowed per client per rate-limit window."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer",
                  "example": 4
                },
                "description": "Requests remaining in the current window."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer",
                  "example": 540
                },
                "description": "Seconds until the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactSuccess"
                }
              }
            }
          },
          "400": {
            "description": "The request body is missing required fields, has an invalid email address, or is not valid JSON.",
            "headers": {
              "API-Version": {
                "schema": {
                  "type": "string",
                  "example": "1"
                }
              },
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer",
                  "example": 5
                }
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer",
                  "example": 4
                }
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer",
                  "example": 540
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this client in the current rate-limit window.",
            "headers": {
              "API-Version": {
                "schema": {
                  "type": "string",
                  "example": "1"
                }
              },
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer",
                  "example": 5
                }
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer",
                  "example": 0
                }
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer",
                  "example": 540
                }
              },
              "Retry-After": {
                "schema": {
                  "type": "integer",
                  "example": 540
                },
                "description": "Seconds to wait before retrying."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "The server failed to send the notification email to the Bonnard team. (A failure to send the requester's confirmation email is logged but does not produce this response, since the request has already succeeded by that point.)",
            "headers": {
              "API-Version": {
                "schema": {
                  "type": "string",
                  "example": "1"
                }
              },
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer",
                  "example": 5
                }
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer",
                  "example": 4
                }
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer",
                  "example": 540
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "contactMethodNotAllowed",
        "summary": "Unsupported method",
        "description": "Only POST is supported on this path. GET, PUT, PATCH, and DELETE all return 405.",
        "responses": {
          "405": {
            "description": "Method not allowed.",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "example": "POST, OPTIONS"
                }
              },
              "API-Version": {
                "schema": {
                  "type": "string",
                  "example": "1"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "options": {
        "operationId": "contactPreflight",
        "summary": "CORS preflight",
        "description": "CORS preflight for the contact endpoint. Returns 204 with an Allow header naming the supported methods. No request body and no response body.",
        "security": [],
        "responses": {
          "204": {
            "description": "No content. The Allow header names the supported methods.",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "example": "POST, OPTIONS"
                },
                "description": "Methods this path accepts."
              },
              "API-Version": {
                "schema": {
                  "type": "string",
                  "example": "1"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ContactRequest": {
        "type": "object",
        "required": [
          "name",
          "email"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Requester's name. Always required."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200,
            "description": "Requester's email address. Always required."
          },
          "company": {
            "type": "string",
            "maxLength": 200,
            "description": "Requester's company name."
          },
          "companySize": {
            "type": "string",
            "maxLength": 200,
            "description": "Requester's company size band."
          },
          "message": {
            "type": "string",
            "maxLength": 5000,
            "description": "Free-text description of what the requester needs help with."
          },
          "source": {
            "type": "string",
            "maxLength": 64,
            "enum": [
              "platform",
              "pricing",
              "agent"
            ],
            "default": "agent",
            "description": "Where the request originated. Agents submitting on a user's behalf should set this to \"agent\". Values other than these are accepted but produce a generic notification."
          },
          "eventId": {
            "type": "string",
            "maxLength": 64,
            "description": "First-party web forms generate this identifier to deduplicate browser and server conversion reports. Agent clients should omit it."
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri",
            "maxLength": 500,
            "description": "First-party web forms send their current page origin and path for conversion context. The server accepts only the current request origin or the configured canonical site origin and removes any query string or fragment. Agent clients should omit it."
          },
          "attribution": {
            "type": "object",
            "description": "How the visitor reached the site, captured from the landing URL. A closed key set: anything else is discarded. Every value is trimmed and capped at 200 characters.",
            "additionalProperties": false,
            "properties": {
              "utm_source": {
                "type": "string",
                "maxLength": 200
              },
              "utm_medium": {
                "type": "string",
                "maxLength": 200
              },
              "utm_campaign": {
                "type": "string",
                "maxLength": 200
              },
              "utm_term": {
                "type": "string",
                "maxLength": 200
              },
              "utm_content": {
                "type": "string",
                "maxLength": 200
              },
              "oai_campaign": {
                "type": "string",
                "maxLength": 200
              },
              "oai_account": {
                "type": "string",
                "maxLength": 200
              },
              "gclid": {
                "type": "string",
                "maxLength": 200
              },
              "li_fat_id": {
                "type": "string",
                "maxLength": 200
              },
              "fbclid": {
                "type": "string",
                "maxLength": 200
              },
              "referrer": {
                "type": "string",
                "maxLength": 200
              },
              "landing_path": {
                "type": "string",
                "maxLength": 200
              }
            }
          }
        }
      },
      "ContactSuccess": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code."
          },
          "hint": {
            "type": "string",
            "description": "What to do to resolve the error."
          }
        }
      }
    }
  }
}
