{
  "openapi": "3.1.0",
  "info": {
    "title": "HeyFox Send API",
    "version": "1.1.0",
    "description": "Send transactional e-mail over HTTP with HeyFox Send (servers in Switzerland). Delivery events are pushed to your webhooks (see `webhooks`), signed after the Standard Webhooks spec. Human-readable docs: https://api.heyfox.email/ — keep in step with server/src/sendapi.ts.",
    "contact": { "name": "HeyFox Send", "email": "hallo@heyfox.email", "url": "https://api.heyfox.email/" }
  },
  "servers": [{ "url": "https://api.heyfox.email/v1" }],
  "security": [{ "apiKey": [] }],
  "components": {
    "securitySchemes": {
      "apiKey": { "type": "http", "scheme": "bearer", "description": "API key hfs_… from the dashboard (Zugänge → API)." }
    },
    "headers": {
      "RequestId": { "description": "Id of this request – quote it to support.", "schema": { "type": "string" } },
      "RateLimitLimit": { "schema": { "type": "integer" } },
      "RateLimitRemaining": { "schema": { "type": "integer" } },
      "RateLimitReset": { "description": "Seconds until the bucket is full again.", "schema": { "type": "integer" } }
    },
    "schemas": {
      "Address": {
        "oneOf": [
          { "type": "string", "description": "\"a@b.de\" or \"Name <a@b.de>\"", "examples": ["Shop <bestellung@dein-shop.de>"] },
          { "type": "object", "required": ["email"], "properties": { "email": { "type": "string", "format": "email" }, "name": { "type": "string", "maxLength": 200 } }, "additionalProperties": false }
        ]
      },
      "Addresses": { "oneOf": [{ "$ref": "#/components/schemas/Address" }, { "type": "array", "items": { "$ref": "#/components/schemas/Address" } }] },
      "Attachment": {
        "type": "object",
        "required": ["filename", "content"],
        "additionalProperties": false,
        "properties": {
          "filename": { "type": "string", "maxLength": 255 },
          "content": { "type": "string", "contentEncoding": "base64" },
          "content_type": { "type": "string", "examples": ["application/pdf"] },
          "content_id": { "type": "string", "description": "Makes it an inline image: <img src=\"cid:…\">", "pattern": "^[\\w.@-]{1,100}$" }
        }
      },
      "SendRequest": {
        "type": "object",
        "required": ["from", "to", "subject"],
        "additionalProperties": false,
        "description": "At most 50 recipients (to, cc and bcc together); html or text required; attachments together at most 15 MB; request at most 25 MB.",
        "properties": {
          "from": { "$ref": "#/components/schemas/Address" },
          "to": { "$ref": "#/components/schemas/Addresses" },
          "cc": { "$ref": "#/components/schemas/Addresses" },
          "bcc": { "$ref": "#/components/schemas/Addresses" },
          "reply_to": { "$ref": "#/components/schemas/Addresses" },
          "subject": { "type": "string", "maxLength": 998 },
          "html": { "type": "string" },
          "text": { "type": "string" },
          "headers": { "type": "object", "maxProperties": 30, "additionalProperties": { "type": "string", "maxLength": 998 }, "description": "Not allowed: From, To, Cc, Bcc, Subject, Reply-To, Sender, Date, Message-ID, Return-Path, MIME-Version, Content-Type, Content-Transfer-Encoding, DKIM-Signature, Received, X-Tenant, Authentication-Results, ARC-*." },
          "attachments": { "type": "array", "maxItems": 20, "items": { "$ref": "#/components/schemas/Attachment" } },
          "tags": { "type": "array", "maxItems": 10, "items": { "type": "string", "pattern": "^[A-Za-z0-9_.:-]{1,64}$" } },
          "metadata": { "type": "object", "maxProperties": 20, "additionalProperties": { "type": ["string", "number", "boolean"] }, "description": "Returned as strings." }
        }
      },
      "SubmitRecipient": {
        "type": "object",
        "required": ["email", "type", "status"],
        "properties": {
          "email": { "type": "string" },
          "type": { "enum": ["to", "cc", "bcc"] },
          "status": { "enum": ["queued", "suppressed", "rejected"] },
          "reason": { "type": "string", "description": "suppressed: bounce | complaint | manual · rejected: daily_limit | recipient_rejected" },
          "detail": { "type": "string" }
        }
      },
      "SendResponse": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "examples": ["msg_8k2m4p9qa7bcx3dfr6tw2ynz"] },
          "status": { "const": "queued" },
          "created_at": { "type": "string", "format": "date-time" },
          "recipients": { "type": "array", "items": { "$ref": "#/components/schemas/SubmitRecipient" } }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "status": { "enum": ["delivered", "sending", "deferred", "partially_delivered", "failed"] },
          "created_at": { "type": "string", "format": "date-time" },
          "from_domain": { "type": "string" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "metadata": { "type": "object", "additionalProperties": { "type": "string" } },
          "recipients": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "email": { "type": "string" },
                "type": { "enum": ["to", "cc", "bcc"] },
                "status": { "enum": ["queued", "accepted", "deferred", "delivered", "bounced", "expired", "complained", "suppressed", "rejected"] },
                "reason": { "type": "string", "description": "e.g. InvalidRecipient, QuotaIssues, SpamBlock (bounced) or the submission reason" },
                "detail": { "type": "string", "description": "Answer of the receiving server (addresses masked)" },
                "updated_at": { "type": "string", "format": "date-time" }
              }
            }
          }
        }
      },
      "MessageList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "status": { "type": "string" },
                "created_at": { "type": "string", "format": "date-time" },
                "from_domain": { "type": "string" },
                "recipients": { "type": "integer" },
                "tags": { "type": "array", "items": { "type": "string" } },
                "metadata": { "type": "object", "additionalProperties": { "type": "string" } }
              }
            }
          },
          "has_more": { "type": "boolean" },
          "next_before": { "type": ["string", "null"] }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "status": { "enum": ["active", "pending_release", "paused", "blocked"] },
          "daily_limit": { "type": "integer" },
          "sent_last_24h": { "type": "integer" },
          "domains": { "type": "array", "items": { "type": "object", "properties": { "domain": { "type": "string" }, "verified": { "type": "boolean" } } } }
        }
      },
      "Domains": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "domain": { "type": "string" },
                "verified": { "type": "boolean" },
                "checked_at": { "type": ["string", "null"], "format": "date-time" },
                "created_at": { "type": "string", "format": "date-time" },
                "dns_records": { "type": "array", "items": { "type": "object", "properties": { "type": { "enum": ["TXT", "CNAME"] }, "name": { "type": "string" }, "value": { "type": "string" } } } }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "message_de"],
            "properties": {
              "code": {
                "enum": ["unauthorized", "rate_limited", "invalid_json", "unsupported_media_type", "payload_too_large", "invalid_request", "unknown_field", "too_many_recipients", "attachments_too_large", "domain_not_verified", "all_recipients_rejected", "account_not_active", "account_paused", "account_blocked", "account_initialising", "daily_limit_reached", "idempotency_key_reused", "idempotency_in_progress", "not_found", "temporarily_unavailable", "submission_failed", "internal_error"]
              },
              "message": { "type": "string" },
              "message_de": { "type": "string" },
              "field": { "type": "string" },
              "detail": { "type": "string" },
              "docs": { "type": "string", "format": "uri" }
            }
          }
        }
      },
      "WebhookEventData": {
        "type": "object",
        "required": [
          "account_id",
          "message_id",
          "smtp_message_id",
          "recipient",
          "from_domain",
          "status",
          "reason",
          "bounce_type",
          "detail",
          "tags",
          "metadata",
          "submitted_at"
        ],
        "properties": {
          "account_id": { "type": "string", "format": "uuid" },
          "message_id": {
            "type": ["string", "null"],
            "description": "msg_… from POST /v1/messages (also test mails); null for SMTP mails",
            "examples": ["msg_5gq8kz2nw7hx4tcr9mfp3jdv"]
          },
          "smtp_message_id": {
            "type": ["string", "null"],
            "description": "Message-ID header of a mail sent over SMTP; null for API mails",
            "examples": ["<abc123@dein-shop.de>"]
          },
          "recipient": { "type": ["string", "null"], "format": "email", "description": "One event per recipient" },
          "from_domain": { "type": ["string", "null"] },
          "status": { "type": "string", "enum": ["delivered", "deferred", "bounced", "expired", "complained"] },
          "reason": {
            "type": ["string", "null"],
            "description": "Bounce class (InvalidRecipient, BadDomain, InactiveMailbox, QuotaIssues, SpamBlock, MessageExpired …), \"complaint\" for complaints, null when delivered"
          },
          "bounce_type": {
            "type": ["string", "null"],
            "enum": ["hard", "soft", null],
            "description": "Only for message.bounced: hard = address went onto the suppression list"
          },
          "detail": { "type": ["string", "null"], "description": "Answer of the receiving server, e-mail addresses masked" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "metadata": { "type": "object", "additionalProperties": { "type": "string" } },
          "submitted_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": ["id", "type", "created_at", "data"],
        "properties": {
          "id": {
            "type": "string",
            "description": "Event id, same as the webhook-id header – use it to drop duplicates",
            "examples": ["evt_7k2fq9mxt4hc3wnp8db6jrse"]
          },
          "type": { "type": "string", "enum": ["message.delivered", "message.deferred", "message.bounced", "message.complained", "webhook.test"] },
          "created_at": { "type": "string", "format": "date-time", "description": "When it happened (delivery order is not guaranteed)" },
          "data": {
            "oneOf": [{ "$ref": "#/components/schemas/WebhookEventData" }, { "$ref": "#/components/schemas/WebhookTestData" }],
            "description": "WebhookTestData only for webhook.test"
          }
        }
      },
      "WebhookTestData": {
        "type": "object",
        "required": ["account_id", "webhook_id", "message"],
        "properties": {
          "account_id": { "type": "string", "format": "uuid" },
          "webhook_id": { "type": "string", "format": "uuid" },
          "message": { "type": "string" }
        }
      }
    },
    "responses": {
      "Error": { "description": "Error with a fixed code", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  },
  "paths": {
    "/messages": {
      "post": {
        "operationId": "sendMessage",
        "summary": "Send one message",
        "parameters": [{ "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 255 }, "description": "Same key + same body within 24 h returns the stored answer (header Idempotent-Replayed: true) instead of sending again." }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } } },
        "responses": {
          "202": { "description": "Accepted for delivery", "headers": { "HeyFox-Request-Id": { "$ref": "#/components/headers/RequestId" }, "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" }, "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" }, "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendResponse" } } } },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "413": { "$ref": "#/components/responses/Error" },
          "415": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/Error" },
          "502": { "$ref": "#/components/responses/Error" },
          "503": { "$ref": "#/components/responses/Error" }
        }
      },
      "get": {
        "operationId": "listMessages",
        "summary": "Latest messages sent via the API",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } },
          { "name": "tag", "in": "query", "schema": { "type": "string" } },
          { "name": "before", "in": "query", "schema": { "type": "string" }, "description": "next_before of the previous page" }
        ],
        "responses": { "200": { "description": "Page of messages", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageList" } } } }, "400": { "$ref": "#/components/responses/Error" }, "401": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/messages/{id}": {
      "get": {
        "operationId": "getMessage",
        "summary": "Delivery state per recipient (30 days)",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^msg_[0-9a-z]{16,40}$" } }],
        "responses": { "200": { "description": "Message", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Message" } } } }, "401": { "$ref": "#/components/responses/Error" }, "404": { "$ref": "#/components/responses/Error" } }
      }
    },
    "/account": {
      "get": { "operationId": "getAccount", "summary": "Account state, daily limit, sender domains", "responses": { "200": { "description": "Account", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Account" } } } }, "401": { "$ref": "#/components/responses/Error" } } }
    },
    "/domains": {
      "get": { "operationId": "listDomains", "summary": "Sender domains with their DNS records", "responses": { "200": { "description": "Domains", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Domains" } } } }, "401": { "$ref": "#/components/responses/Error" } } }
    }
  },
  "webhooks": {
    "message.delivered": {
      "post": {
        "summary": "The receiving server accepted the mail",
        "description": "Sent for every mail of the account – API and SMTP. Answer 2xx within 10 seconds; anything else (also 3xx) is retried after 1, 5, 30 min, 2, 6, 12 and 24 h. An address that fails for 3 days is switched off and the account owner gets an e-mail.",
        "parameters": [
          { "name": "webhook-id", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Event id (evt_…)" },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Unix seconds when sent; reject if more than 5 minutes off"
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "\"v1,<base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`>\" – key: the base64-decoded part of your whsec_ secret after the prefix. Several signatures are separated by spaces."
          },
          { "name": "HeyFox-Event", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The event type" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEvent" } } } },
        "responses": { "2XX": { "description": "Event stored" } },
        "security": []
      }
    },
    "message.deferred": {
      "post": {
        "summary": "The receiving server asked to try later",
        "description": "Once per mail, at the first delay; HeyFox Send keeps trying for 24 hours. Answer 2xx within 10 seconds; anything else (also 3xx) is retried after 1, 5, 30 min, 2, 6, 12 and 24 h. An address that fails for 3 days is switched off and the account owner gets an e-mail.",
        "parameters": [
          { "name": "webhook-id", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Event id (evt_…)" },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Unix seconds when sent; reject if more than 5 minutes off"
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "\"v1,<base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`>\" – key: the base64-decoded part of your whsec_ secret after the prefix. Several signatures are separated by spaces."
          },
          { "name": "HeyFox-Event", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The event type" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEvent" } } } },
        "responses": { "2XX": { "description": "Event stored" } },
        "security": []
      }
    },
    "message.bounced": {
      "post": {
        "summary": "Rejected or given up",
        "description": "status bounced (rejected) or expired (given up after 24 hours); bounce_type hard puts the address on the suppression list. Answer 2xx within 10 seconds; anything else (also 3xx) is retried after 1, 5, 30 min, 2, 6, 12 and 24 h. An address that fails for 3 days is switched off and the account owner gets an e-mail.",
        "parameters": [
          { "name": "webhook-id", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Event id (evt_…)" },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Unix seconds when sent; reject if more than 5 minutes off"
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "\"v1,<base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`>\" – key: the base64-decoded part of your whsec_ secret after the prefix. Several signatures are separated by spaces."
          },
          { "name": "HeyFox-Event", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The event type" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEvent" } } } },
        "responses": { "2XX": { "description": "Event stored" } },
        "security": []
      }
    },
    "message.complained": {
      "post": {
        "summary": "The recipient marked the mail as spam",
        "description": "The address is on the suppression list from now on. Answer 2xx within 10 seconds; anything else (also 3xx) is retried after 1, 5, 30 min, 2, 6, 12 and 24 h. An address that fails for 3 days is switched off and the account owner gets an e-mail.",
        "parameters": [
          { "name": "webhook-id", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Event id (evt_…)" },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Unix seconds when sent; reject if more than 5 minutes off"
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "\"v1,<base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`>\" – key: the base64-decoded part of your whsec_ secret after the prefix. Several signatures are separated by spaces."
          },
          { "name": "HeyFox-Event", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The event type" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEvent" } } } },
        "responses": { "2XX": { "description": "Event stored" } },
        "security": []
      }
    },
    "webhook.test": {
      "post": {
        "summary": "Test event",
        "description": "Only from \"Test senden\" in the dashboard; data holds account_id, webhook_id and a message. Answer 2xx within 10 seconds; anything else (also 3xx) is retried after 1, 5, 30 min, 2, 6, 12 and 24 h. An address that fails for 3 days is switched off and the account owner gets an e-mail.",
        "parameters": [
          { "name": "webhook-id", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Event id (evt_…)" },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "Unix seconds when sent; reject if more than 5 minutes off"
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "schema": { "type": "string" },
            "description": "\"v1,<base64 HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{raw body}`>\" – key: the base64-decoded part of your whsec_ secret after the prefix. Several signatures are separated by spaces."
          },
          { "name": "HeyFox-Event", "in": "header", "required": true, "schema": { "type": "string" }, "description": "The event type" }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEvent" } } } },
        "responses": { "2XX": { "description": "Event stored" } },
        "security": []
      }
    }
  }
}
