{
  "openapi": "3.1.0",
  "info": {
    "title": "Duva API",
    "description": "Transactional email API, hosted in Canada. Send email from your own domains over HTTPS, then read delivery events, manage suppressions, receive signed webhooks and read statistics.\n\n**Authentication.** One API key per domain, in `Authorization: Bearer dv_...`; the `{domain}` of the path must be the key's domain. No key: `401`. Any key that does not authenticate for that domain (unknown, wrong, revoked, expired, other domain): the same `404`, without saying why.\n\n**Errors.** Always `{\"error\": {\"code\", \"message\", \"fields\"?}}`; `code` is the contract. `Accept-Language: en` gives English texts (French otherwise).\n\n**Rate limits.** A per-key limit protects the service; on `429`, wait `Retry-After` seconds before retrying.\n\n**Extensions read by client generators.** `x-retryable` (on an error response): whether that specific error may be retried automatically (`false`, `true`, or an object keyed by `error.code` when a status covers more than one code). `x-safe-retry` (on an operation): whether the whole call may be retried after a network failure or no response. `x-idempotent` (on an operation, where meaningful): whether calling it twice with the same arguments has the same effect as once. `x-pagination` (on a paginated operation): `{cursor_param, next_field}`.\n\n**Compatibility.** Adding a field, an enum value, or an operation is NOT a breaking change: unknown fields and enum values must be ignored, never rejected. A breaking change (removing or renaming a field, tightening a constraint) is announced in the Changelog section of the reference at least 6 months before the old shape stops being accepted.\n\nFull reference and guides: https://duva.ca/en/docs",
    "version": "1",
    "contact": {
      "name": "Duva",
      "url": "https://duva.ca"
    }
  },
  "servers": [
    {
      "url": "https://api.duva.ca",
      "description": "The API address shown in your dashboard."
    }
  ],
  "paths": {
    "/v1/{domain}/messages": {
      "post": {
        "tags": [
          "messages"
        ],
        "summary": "Send a message",
        "operationId": "sendMessage",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255,
              "pattern": "^[\\x21-\\x7E]+$"
            },
            "description": "Optional, 1 to 255 printable ASCII characters without spaces, unique per domain. The same key with the same request replays the first answer (`Idempotent-Replayed: true`, no quota consumed); the same key with a different request gives `409`."
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted: validated and queued for delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageAccepted"
                },
                "example": {
                  "id": "msg_9f1c2d3e4a5b46c78d9e0f1a2b3c4d5e",
                  "status": "queued"
                }
              }
            },
            "headers": {
              "Location": {
                "description": "Address of the message (`GET /v1/{domain}/messages/{id}`).",
                "schema": {
                  "type": "string"
                }
              },
              "Idempotent-Replayed": {
                "description": "`true` when this answer replays an earlier identical request.",
                "schema": {
                  "type": "string",
                  "const": "true"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "429": {
            "$ref": "#/components/responses/SendTooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Accepts a message for delivery. The response is always asynchronous: it never confirms a delivery, only that the message was validated and queued. One copy is sent per recipient. Read the outcome with `getMessage`, `listEvents` or a webhook.\n\nUnusual volume for the account can cause a message to be held for verification before it is sent; the response is the same (`202`, `queued`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMessageRequest"
              }
            }
          }
        },
        "x-idempotent": true,
        "x-safe-retry": true
      }
    },
    "/v1/{domain}/messages/{message_id}": {
      "get": {
        "tags": [
          "messages"
        ],
        "summary": "Get a message and its recipients' statuses",
        "description": "The message status is that of its least advanced recipient (`queued` < `sent` < `delivered` < `bounced` < `failed`). The body and headers of the message are never returned. A message that does not exist, is malformed, or belongs to another domain gives the same `404`.",
        "operationId": "getMessage",
        "parameters": [
          {
            "name": "message_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A message id (`msg_` followed by 32 hexadecimal characters)."
          },
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Message"
                },
                "example": {
                  "id": "msg_9f1c2d3e4a5b46c78d9e0f1a2b3c4d5e",
                  "status": "sent",
                  "from": "notifications@example.com",
                  "subject": "Your order",
                  "tags": [
                    "order"
                  ],
                  "metadata": {
                    "order_id": "A-1042"
                  },
                  "tracking": {
                    "opens": false,
                    "clicks": false
                  },
                  "created_at": "2026-09-19T14:03:21.512Z",
                  "recipients": [
                    {
                      "email": "a@example.org",
                      "type": "to",
                      "name": "Alex Martin",
                      "status": "delivered",
                      "updated_at": "2026-09-19T14:03:24.101Z"
                    },
                    {
                      "email": "b@example.org",
                      "type": "cc",
                      "name": null,
                      "status": "sent",
                      "updated_at": "2026-09-19T14:03:22.870Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-safe-retry": true
      }
    },
    "/v1/{domain}/events": {
      "get": {
        "tags": [
          "events"
        ],
        "summary": "List delivery events",
        "operationId": "listEvents",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          },
          {
            "name": "message_id",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 64
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "A message id (`msg_` followed by 32 hexadecimal characters)."
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/EventType"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Only events of this type."
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 254
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Only events for this address (case-insensitive)."
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date-time"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Only from this instant (ISO 8601, time zone REQUIRED, between 1970 and 2200)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 1,
              "default": 50
            },
            "description": "Page size (1 to 100, 50 by default)."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 200
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "The `next_cursor` of the previous page. Opaque value."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventPage"
                },
                "example": {
                  "data": [
                    {
                      "id": "evt_0b1c2d3e4f5a46b78c9d0e1f2a3b4c5d",
                      "type": "bounced",
                      "message_id": "msg_9f1c2d3e4a5b46c78d9e0f1a2b3c4d5e",
                      "recipient": "b@example.org",
                      "occurred_at": "2026-09-19T14:03:25.310Z",
                      "detail": {
                        "code": 550,
                        "enhanced_code": "5.1.1",
                        "message": "user unknown",
                        "classification": "InvalidRecipient",
                        "attempts": 0
                      },
                      "metadata": {
                        "order_id": "A-1042"
                      }
                    }
                  ],
                  "next_cursor": "MjAyNi0wOS0xOVQxNDowMzoyNS4zMTAr..."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Delivery events of the domain, most recent first, by pages. Follow `next_cursor` until it is `null`. Events are delivered at least once by the sending server; Duva deduplicates them, so each appears once. `detail.message` is the first line of the remote server's reply: external text, never to be interpreted.",
        "x-safe-retry": true,
        "x-pagination": {
          "cursor_param": "cursor",
          "next_field": "next_cursor"
        }
      }
    },
    "/v1/{domain}/suppressions": {
      "get": {
        "tags": [
          "suppressions"
        ],
        "summary": "List suppressed addresses",
        "operationId": "listSuppressions",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          },
          {
            "name": "reason",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/SuppressionReason"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Only suppressions with this reason."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 1,
              "default": 50
            },
            "description": "Page size (1 to 100, 50 by default)."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "maxLength": 400
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "The `next_cursor` of the previous page. Opaque value."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuppressionPage"
                },
                "example": {
                  "data": [
                    {
                      "email": "b@example.org",
                      "reason": "bounce",
                      "message_id": "msg_9f1c2d3e4a5b46c78d9e0f1a2b3c4d5e",
                      "created_at": "2026-09-19T14:03:25.310Z"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Addresses this domain no longer writes to: hard bounces, complaints, unsubscribes and manual additions. A suppression in one domain does not apply to another.",
        "x-safe-retry": true,
        "x-pagination": {
          "cursor_param": "cursor",
          "next_field": "next_cursor"
        }
      },
      "post": {
        "tags": [
          "suppressions"
        ],
        "summary": "Suppress an address",
        "description": "Adds an address by hand (reason `manual`): it will receive nothing more from this domain. `201` if added, `200` (with the existing entry) if it was already there.",
        "operationId": "addSuppression",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          }
        ],
        "responses": {
          "200": {
            "description": "The address was already on the list (existing entry).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Suppression"
                },
                "example": {
                  "email": "client@example.org",
                  "reason": "manual",
                  "message_id": null,
                  "created_at": "2026-09-19T14:03:25.310Z"
                }
              }
            }
          },
          "201": {
            "description": "The address was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Suppression"
                },
                "example": {
                  "email": "client@example.org",
                  "reason": "manual",
                  "message_id": null,
                  "created_at": "2026-09-19T14:03:25.310Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddSuppressionRequest"
              }
            }
          }
        },
        "x-idempotent": true,
        "x-safe-retry": false
      }
    },
    "/v1/{domain}/suppressions/{email}": {
      "delete": {
        "tags": [
          "suppressions"
        ],
        "summary": "Remove a suppressed address",
        "operationId": "removeSuppression",
        "parameters": [
          {
            "name": "email",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The email address, URL-encoded."
          },
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          }
        ],
        "responses": {
          "204": {
            "description": "No content."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "The address may receive mail again. `404` if it is not on the list (or only on another domain's list: indistinguishable). Removing a bounce or a complaint is your responsibility: writing again to an address that bounces harms sending reputation.",
        "x-idempotent": true,
        "x-safe-retry": false
      }
    },
    "/v1/{domain}/webhooks": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Register a webhook endpoint",
        "description": "Duva will `POST` each delivery event to this URL (see the `deliveryEvent` webhook of this document). The response carries the signing `secret` (`whsec_...`): it is shown ONCE, keep it to verify signatures. The URL must be `https://` and reach the public Internet (private, local and internal addresses are refused with `422`); five webhooks at most per domain (`409` `limit_reached`).",
        "operationId": "createWebhook",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The response carries the `secret`, once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                },
                "example": {
                  "id": "wh_1a2b3c4d5e6f47a89b0c1d2e3f4a5b6c",
                  "url": "https://example.org/hooks/duva",
                  "events": [
                    "delivered",
                    "bounced"
                  ],
                  "status": "active",
                  "disabled_reason": null,
                  "created_at": "2026-09-19T14:03:21.512Z",
                  "secret": "whsec_MfKQ9r8GKYqrTwjQPDjdE9ZUgxsBl9GXVEDjeejrQiI="
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/LimitReached"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              }
            }
          }
        },
        "x-idempotent": false,
        "x-safe-retry": false
      },
      "get": {
        "tags": [
          "webhooks"
        ],
        "summary": "List webhook endpoints",
        "operationId": "listWebhooks",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookList"
                },
                "example": {
                  "data": [
                    {
                      "id": "wh_1a2b3c4d5e6f47a89b0c1d2e3f4a5b6c",
                      "url": "https://example.org/hooks/duva",
                      "events": [
                        "delivered",
                        "bounced"
                      ],
                      "status": "active",
                      "disabled_reason": null,
                      "created_at": "2026-09-19T14:03:21.512Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "The secret is never returned after creation.",
        "x-safe-retry": true
      }
    },
    "/v1/{domain}/webhooks/{webhook_id}": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "summary": "Get a webhook endpoint",
        "operationId": "getWebhook",
        "parameters": [
          {
            "name": "webhook_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A webhook id (`wh_` followed by 32 hexadecimal characters)."
          },
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                },
                "example": {
                  "id": "wh_1a2b3c4d5e6f47a89b0c1d2e3f4a5b6c",
                  "url": "https://example.org/hooks/duva",
                  "events": [
                    "delivered",
                    "bounced"
                  ],
                  "status": "active",
                  "disabled_reason": null,
                  "created_at": "2026-09-19T14:03:21.512Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "`status` is `active` or `disabled`; `disabled_reason` says why (for example `410 Gone`, or too many consecutive failures).",
        "x-safe-retry": true
      },
      "delete": {
        "tags": [
          "webhooks"
        ],
        "summary": "Delete a webhook endpoint",
        "operationId": "deleteWebhook",
        "parameters": [
          {
            "name": "webhook_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A webhook id (`wh_` followed by 32 hexadecimal characters)."
          },
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          }
        ],
        "responses": {
          "204": {
            "description": "No content."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Pending deliveries to this endpoint are abandoned.",
        "x-idempotent": true,
        "x-safe-retry": false
      }
    },
    "/v1/{domain}/webhooks/{webhook_id}/deliveries": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "summary": "List the latest deliveries of a webhook endpoint",
        "description": "Status, attempts and last HTTP status code of each delivery. `last_error` is a fixed text (`HTTP 500`, `connection failed`...): the body of your response is never kept.",
        "operationId": "listWebhookDeliveries",
        "parameters": [
          {
            "name": "webhook_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A webhook id (`wh_` followed by 32 hexadecimal characters)."
          },
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 1,
              "default": 50
            },
            "description": "Page size (1 to 100, 50 by default)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryList"
                },
                "example": {
                  "data": [
                    {
                      "id": "dlv_2b3c4d5e6f7a48b90c1d2e3f4a5b6c7d",
                      "event_id": "evt_0b1c2d3e4f5a46b78c9d0e1f2a3b4c5d",
                      "status": "delivered",
                      "attempts": 1,
                      "last_status_code": 200,
                      "last_error": null,
                      "created_at": "2026-09-19T14:03:25.400Z",
                      "delivered_at": "2026-09-19T14:03:25.610Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-safe-retry": true
      }
    },
    "/v1/{domain}/stats": {
      "get": {
        "tags": [
          "stats"
        ],
        "summary": "Get delivery statistics",
        "operationId": "getStats",
        "parameters": [
          {
            "name": "domain",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The sending domain the API key was created for (for example `example.com`)."
          },
          {
            "name": "granularity",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "day",
                "hour"
              ],
              "type": "string",
              "default": "day"
            },
            "description": "`day` (default) or `hour`. Periods are in UTC."
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date-time"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Only from this instant (ISO 8601, time zone REQUIRED, between 1970 and 2200)."
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "format": "date-time"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Up to (excluding) this instant (ISO 8601, time zone REQUIRED)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                },
                "example": {
                  "granularity": "day",
                  "since": "2026-09-01T00:00:00.000Z",
                  "until": "2026-09-19T20:00:00.000Z",
                  "data": [
                    {
                      "period": "2026-09-19T00:00:00.000Z",
                      "accepted": 120,
                      "suppressed": 3,
                      "delivered": 110,
                      "bounced": 4,
                      "deferred": 9,
                      "expired": 0,
                      "complained": 1,
                      "opened": 40,
                      "clicked": 12
                    }
                  ],
                  "totals": {
                    "accepted": 120,
                    "suppressed": 3,
                    "delivered": 110,
                    "bounced": 4,
                    "deferred": 9,
                    "expired": 0,
                    "complained": 1,
                    "opened": 40,
                    "clicked": 12
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "description": "Counters of the domain by period (UTC), from the accepted recipients and the event log. Periods without activity are present, at zero. At most 366 days, or 7 days by hour (`422` beyond). `accepted` counts recipients at the acceptance date of the message (suppressed ones are counted in `suppressed`); the other counters count events at the date they occurred.",
        "x-safe-retry": true
      }
    },
    "/health": {
      "get": {
        "summary": "Health check",
        "operationId": "getHealth",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {},
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "503": {
            "description": "The database is unreachable."
          }
        },
        "tags": [
          "health"
        ],
        "description": "`200` when the service works, `503` when its database is unreachable.",
        "x-safe-retry": true,
        "security": []
      }
    }
  },
  "components": {
    "schemas": {
      "Attachment": {
        "additionalProperties": false,
        "description": "Une pièce jointe : contenu en base64. Règles (nom, type, taille) : `services.messages`.",
        "properties": {
          "filename": {
            "maxLength": 1000,
            "type": "string",
            "description": "Required, 255 characters at most, without a path."
          },
          "content": {
            "maxLength": 7000000,
            "type": "string",
            "description": "Required, base64."
          },
          "content_type": {
            "anyOf": [
              {
                "maxLength": 1000,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Optional, `application/octet-stream` by default."
          },
          "content_id": {
            "anyOf": [
              {
                "maxLength": 1000,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Optional: makes the attachment an inline image that the HTML names with `cid:`."
          }
        },
        "required": [
          "filename",
          "content"
        ],
        "type": "object"
      },
      "WebhookDeliveryList": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            },
            "type": "array"
          }
        },
        "type": "object",
        "required": [
          "data"
        ]
      },
      "WebhookDelivery": {
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^dlv_[0-9a-f]{32}$"
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed"
            ]
          },
          "attempts": {
            "type": "integer"
          },
          "last_status_code": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          },
          "last_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "delivered_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "required": [
          "id",
          "event_id",
          "status",
          "attempts",
          "last_status_code",
          "last_error",
          "created_at",
          "delivered_at"
        ]
      },
      "Error": {
        "type": "object",
        "description": "Every error has this shape, without internal detail or received value.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "not_found",
                  "domain_not_verified",
                  "sending_not_allowed",
                  "idempotency_conflict",
                  "limit_reached",
                  "payload_too_large",
                  "invalid_request",
                  "quota_exceeded",
                  "rate_limited",
                  "internal_error",
                  "method_not_allowed",
                  "http_error"
                ],
                "description": "The contract: rely on it, and on `fields[].field`, in your code."
              },
              "message": {
                "type": "string",
                "description": "Readable text in the language of the `Accept-Language` header (`en` or `fr`; French by default). The wording may change; the status, `code` and `fields[].field` never do."
              },
              "fields": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "field",
                    "message"
                  ],
                  "properties": {
                    "field": {
                      "type": "string",
                      "examples": [
                        "to[0]"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Event": {
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$"
          },
          "type": {
            "$ref": "#/components/schemas/EventType"
          },
          "message_id": {
            "type": "string",
            "pattern": "^msg_[0-9a-f]{32}$"
          },
          "recipient": {
            "type": "string"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "detail": {
            "additionalProperties": true,
            "type": "object",
            "description": "Chosen fields only: `code`, `enhanced_code`, `message` (external text: never interpret it), `classification`, `attempts`, `feedback_type` for a complaint, `url` for a click. Never the content of the message or its headers."
          },
          "metadata": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object"
          }
        },
        "type": "object",
        "required": [
          "id",
          "type",
          "message_id",
          "recipient",
          "occurred_at",
          "detail",
          "metadata"
        ]
      },
      "EventPage": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Event"
            },
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Pass it as `cursor` for the next page; `null` on the last page."
          }
        },
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "EventType": {
        "type": "string",
        "enum": [
          "delivered",
          "bounced",
          "deferred",
          "expired",
          "complained",
          "opened",
          "clicked"
        ],
        "description": "`delivered` (the recipient's server accepted the message), `bounced` (permanent refusal, including one received afterwards), `deferred` (temporary failure: the sending server will retry), `expired` (it gave up after 24 h), `complained` (the recipient reported the message: the address is then suppressed), `opened` and `clicked` (from Duva's tracking, not from the sending server)."
      },
      "MessageAccepted": {
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^msg_[0-9a-f]{32}$"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "bounced",
              "failed"
            ],
            "description": "`queued`, or `failed` when ALL recipients are on the suppression list."
          }
        },
        "type": "object",
        "required": [
          "id",
          "status"
        ]
      },
      "SendMessageRequest": {
        "additionalProperties": false,
        "description": "Forme de la requête. Les règles de contenu (adresses, en-têtes...) : `services.messages`.\n\n`extra=\"forbid\"` : une faute de frappe (`replyto`, `subjet`) est refusée, pas ignorée en\nsilence.\nLes bornes ci-dessous ne servent qu'à limiter le travail avant validation.",
        "properties": {
          "from": {
            "maxLength": 1000,
            "type": "string",
            "description": "Required. `address` or `Name <address>`. Its domain MUST be the one in the path (subdomains excluded)."
          },
          "to": {
            "items": {
              "type": "string"
            },
            "maxItems": 1000,
            "minItems": 1,
            "type": "array",
            "description": "Required. Each entry is an `address` or a `Name <address>`; de-duplicated case-insensitively. `to`, `cc` and `bcc` together count against the maximum of the plan (5 in the sandbox). One copy per address, and every copy shows all the `to` and all the `cc`."
          },
          "cc": {
            "items": {
              "type": "string"
            },
            "maxItems": 1000,
            "type": "array",
            "description": "Optional, same format as `to`. Shown to every recipient. An address already in `to` is ignored."
          },
          "bcc": {
            "items": {
              "type": "string"
            },
            "maxItems": 1000,
            "type": "array",
            "description": "Optional, same format as `to`. Never shown to the other recipients: only its own copy carries a `Bcc` header, with its address alone. An address already in `to` or `cc` is ignored."
          },
          "subject": {
            "maxLength": 5000,
            "type": "string",
            "description": "Required, one line, 998 characters at most, no control characters."
          },
          "html": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "At least one of `html` and `text`. 2 MB in all. No NUL character."
          },
          "text": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "At least one of `html` and `text`. 2 MB in all. No NUL character."
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "type": "array",
            "description": "10 at most, 64 characters each, no leading or trailing space. De-duplicated."
          },
          "tracking": {
            "$ref": "#/components/schemas/Tracking",
            "description": "Open and click tracking, OFF by default. Requires an `html` body and a domain where Duva has enabled tracking, otherwise `422` (field `tracking`)."
          },
          "reply_to": {
            "anyOf": [
              {
                "maxLength": 1000,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "`address` or `Name <address>`."
          },
          "headers": {
            "additionalProperties": {
              "type": "string"
            },
            "maxProperties": 100,
            "type": "object",
            "description": "20 headers at most, single-line values. Allow-list: `List-Unsubscribe`, `List-Unsubscribe-Post`, `List-Id`, `In-Reply-To`, `References`, `Auto-Submitted`, `Precedence`, `Importance`, `Feedback-ID` and any `X-*` except the reserved prefixes. `From`, `To`, `Cc`, `Bcc`, `Return-Path`, `Message-ID`, `DKIM-Signature`... are refused."
          },
          "metadata": {
            "additionalProperties": {
              "type": "string"
            },
            "maxProperties": 100,
            "type": "object",
            "description": "Text to text, NEVER sent to the recipient: returned by `getMessage`, in each event and in the webhooks of this message. 10 keys at most (letters, digits, `_`, `.`, `-`, 40 characters), values of 500 characters on one line."
          },
          "attachments": {
            "items": {
              "$ref": "#/components/schemas/Attachment"
            },
            "maxItems": 100,
            "type": "array",
            "description": "10 at most, 5 MB decoded in all. Executable extensions are refused. Reserved to accounts in production."
          }
        },
        "required": [
          "from",
          "to",
          "subject"
        ],
        "type": "object",
        "example": {
          "from": "Example <notifications@example.com>",
          "to": [
            "client@example.org"
          ],
          "subject": "Your order",
          "html": "<p>Thank you for your order.</p>",
          "text": "Thank you for your order.",
          "tags": [
            "order"
          ],
          "metadata": {
            "order_id": "A-1042"
          }
        }
      },
      "Message": {
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^msg_[0-9a-f]{32}$"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "bounced",
              "failed"
            ],
            "description": "That of the least advanced recipient."
          },
          "from": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "metadata": {
            "additionalProperties": {
              "type": "string"
            },
            "type": "object"
          },
          "tracking": {
            "$ref": "#/components/schemas/Tracking"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "recipients": {
            "items": {
              "$ref": "#/components/schemas/Recipient"
            },
            "type": "array"
          }
        },
        "type": "object",
        "required": [
          "id",
          "status",
          "from",
          "subject",
          "tags",
          "metadata",
          "tracking",
          "created_at",
          "recipients"
        ]
      },
      "StatsPeriod": {
        "properties": {
          "period": {
            "type": "string",
            "format": "date-time"
          },
          "accepted": {
            "type": "integer"
          },
          "suppressed": {
            "type": "integer"
          },
          "delivered": {
            "type": "integer"
          },
          "bounced": {
            "type": "integer"
          },
          "deferred": {
            "type": "integer"
          },
          "expired": {
            "type": "integer"
          },
          "complained": {
            "type": "integer"
          },
          "opened": {
            "type": "integer"
          },
          "clicked": {
            "type": "integer"
          }
        },
        "type": "object",
        "required": [
          "period",
          "accepted",
          "suppressed",
          "delivered",
          "bounced",
          "deferred",
          "expired",
          "complained",
          "opened",
          "clicked"
        ]
      },
      "Recipient": {
        "properties": {
          "email": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "to",
              "cc",
              "bcc"
            ],
            "description": "`to`, `cc` or `bcc`: how the address was given in the request."
          },
          "name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The name given with `Name <address>`; `null` for a bare address."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sent",
              "delivered",
              "bounced",
              "failed",
              "suppressed"
            ]
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "type": "object",
        "required": [
          "email",
          "type",
          "name",
          "status",
          "updated_at"
        ]
      },
      "Stats": {
        "properties": {
          "granularity": {
            "type": "string",
            "enum": [
              "day",
              "hour"
            ]
          },
          "since": {
            "type": "string",
            "format": "date-time"
          },
          "until": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "items": {
              "$ref": "#/components/schemas/StatsPeriod"
            },
            "type": "array"
          },
          "totals": {
            "additionalProperties": {
              "type": "integer"
            },
            "type": "object",
            "description": "The same counters, summed."
          }
        },
        "type": "object",
        "required": [
          "granularity",
          "since",
          "until",
          "data",
          "totals"
        ]
      },
      "AddSuppressionRequest": {
        "additionalProperties": false,
        "properties": {
          "email": {
            "maxLength": 1000,
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "type": "object"
      },
      "Suppression": {
        "properties": {
          "email": {
            "type": "string"
          },
          "reason": {
            "$ref": "#/components/schemas/SuppressionReason"
          },
          "message_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "type": "object",
        "required": [
          "email",
          "reason",
          "message_id",
          "created_at"
        ]
      },
      "SuppressionPage": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Suppression"
            },
            "type": "array"
          },
          "next_cursor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Pass it as `cursor` for the next page; `null` on the last page."
          }
        },
        "type": "object",
        "required": [
          "data",
          "next_cursor"
        ]
      },
      "SuppressionReason": {
        "type": "string",
        "enum": [
          "bounce",
          "unsubscribe",
          "complaint",
          "manual"
        ],
        "description": "`bounce`, `complaint`, `unsubscribe` or `manual`."
      },
      "Tracking": {
        "additionalProperties": false,
        "properties": {
          "opens": {
            "default": false,
            "type": "boolean"
          },
          "clicks": {
            "default": false,
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "WebhookEvent": {
        "type": "object",
        "description": "The JSON body Duva sends to your webhook URL for each delivery event.",
        "required": [
          "id",
          "type",
          "domain",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$"
          },
          "type": {
            "$ref": "#/components/schemas/EventType"
          },
          "domain": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "required": [
              "message_id",
              "recipient",
              "occurred_at",
              "detail",
              "metadata"
            ],
            "properties": {
              "message_id": {
                "type": "string",
                "pattern": "^msg_[0-9a-f]{32}$"
              },
              "recipient": {
                "type": "string"
              },
              "occurred_at": {
                "type": "string",
                "format": "date-time"
              },
              "detail": {
                "type": "object",
                "additionalProperties": true
              },
              "metadata": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          }
        },
        "examples": [
          {
            "id": "evt_0b1c2d3e4f5a46b78c9d0e1f2a3b4c5d",
            "type": "bounced",
            "domain": "example.com",
            "data": {
              "message_id": "msg_9f1c2d3e4a5b46c78d9e0f1a2b3c4d5e",
              "recipient": "b@example.org",
              "occurred_at": "2026-09-19T14:03:25.310Z",
              "detail": {
                "code": 550,
                "enhanced_code": "5.1.1"
              },
              "metadata": {
                "order_id": "A-1042"
              }
            }
          }
        ]
      },
      "CreateWebhookRequest": {
        "additionalProperties": false,
        "properties": {
          "url": {
            "maxLength": 3000,
            "type": "string"
          },
          "events": {
            "items": {
              "type": "string"
            },
            "maxItems": 20,
            "type": "array"
          }
        },
        "required": [
          "url"
        ],
        "type": "object"
      },
      "WebhookList": {
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Webhook"
            },
            "type": "array"
          }
        },
        "type": "object",
        "required": [
          "data"
        ]
      },
      "Webhook": {
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^wh_[0-9a-f]{32}$"
          },
          "url": {
            "type": "string"
          },
          "events": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "disabled_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "secret": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "The signing secret (`whsec_...`): present ONLY in the response that creates the endpoint."
          }
        },
        "type": "object",
        "required": [
          "id",
          "url",
          "events",
          "status",
          "disabled_reason",
          "created_at"
        ]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "No API key presented (or a scheme other than `Bearer`).",
        "x-retryable": false,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "unauthorized",
                "message": "..."
              }
            }
          }
        },
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string",
              "const": "Bearer"
            }
          }
        }
      },
      "Forbidden": {
        "description": "`domain_not_verified` (the domain's DNS is not verified yet) or `sending_not_allowed` (the account or the domain is suspended, or in a state that forbids sending). Only `sendMessage` returns this response.",
        "x-retryable": false,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "domain_not_verified",
                "message": "..."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "An API key that is unknown, wrong, revoked, expired or from another domain, an unknown domain or a closed account all give this same response, without saying which. On reads: the resource does not exist (or is not yours: indistinguishable).",
        "x-retryable": false,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "..."
              }
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "The `Idempotency-Key` was already used for a different request. Only `sendMessage` returns this response.",
        "x-retryable": false,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "idempotency_conflict",
                "message": "..."
              }
            }
          }
        }
      },
      "LimitReached": {
        "description": "The ceiling of webhooks per domain is reached. Only `createWebhook` returns this response.",
        "x-retryable": false,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "limit_reached",
                "message": "..."
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The request is larger than 4 MB (9 MB to send a message, for its attachments).",
        "x-retryable": false,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "payload_too_large",
                "message": "..."
              }
            }
          }
        }
      },
      "InvalidRequest": {
        "description": "Invalid body or parameter. `fields` names the field (`from`, `to[1]`, `headers.Bcc`, `body` when the JSON is unreadable, `limit`, `cursor`...). Unknown fields are refused.",
        "x-retryable": false,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_request",
                "message": "..."
              }
            }
          }
        }
      },
      "QuotaExceeded": {
        "description": "Daily or monthly sending ceiling reached. `Retry-After` is the seconds before the next UTC period, which can be hours: NEVER retry this one automatically.",
        "x-retryable": false,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "quota_exceeded",
                "message": "..."
              }
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before trying again.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests with this key in a short time (unrelated to your sending quota). `Retry-After` is the seconds to wait.",
        "x-retryable": true,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "..."
              }
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before trying again.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        }
      },
      "SendTooManyRequests": {
        "description": "`quota_exceeded` (daily or monthly sending ceiling reached: NEVER retry automatically, `Retry-After` can be hours) or `rate_limited` (too many requests with this key: safe to retry after `Retry-After` seconds). Only `sendMessage` returns this response; every other operation only ever answers `rate_limited` on a `429` (see `RateLimited`).",
        "x-retryable": {
          "quota_exceeded": false,
          "rate_limited": true
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "quota_exceeded": {
                "value": {
                  "error": {
                    "code": "quota_exceeded",
                    "message": "..."
                  }
                }
              },
              "rate_limited": {
                "value": {
                  "error": {
                    "code": "rate_limited",
                    "message": "..."
                  }
                }
              }
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before trying again.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal error.",
        "x-retryable": true,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "internal_error",
                "message": "..."
              }
            }
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "dv_...",
        "description": "An API key created for ONE domain (dashboard, « API keys » screen)."
      }
    }
  },
  "tags": [
    {
      "name": "messages",
      "description": "Send a message and read its status."
    },
    {
      "name": "events",
      "description": "The log of delivery events (delivered, bounced, opened...)."
    },
    {
      "name": "suppressions",
      "description": "Addresses a domain no longer writes to."
    },
    {
      "name": "webhooks",
      "description": "Receive delivery events at a URL of your choice, signed."
    },
    {
      "name": "stats",
      "description": "Counters by period."
    },
    {
      "name": "health",
      "description": "Service health, without authentication."
    }
  ],
  "externalDocs": {
    "description": "API reference",
    "url": "https://duva.ca/en/docs"
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "webhooks": {
    "deliveryEvent": {
      "post": {
        "operationId": "receiveDeliveryEvent",
        "tags": [
          "webhooks"
        ],
        "summary": "A delivery event, sent to your webhook URL",
        "description": "Signature in the Standard Webhooks format: `webhook-signature` is `v1,` followed by `base64(HMAC-SHA256(secret, \"<webhook-id>.<webhook-timestamp>.<raw body>\"))`, where the secret is the base64 part of `whsec_<base64>`. Verify it on the RAW body and refuse a `webhook-timestamp` more than 5 minutes old. Answer `2xx` to acknowledge; `410` disables the webhook; anything else (or a timeout of 10 s) is retried (30 s, doubling up to 1 h, 10 attempts). Redirections are not followed. Delivery is at least once: de-duplicate on `webhook-id`; no order is guaranteed between events.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The event id (`evt_...`), identical across retries.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix time of THIS attempt, in seconds.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 signature>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          },
          "410": {
            "description": "Gone: Duva disables this webhook."
          }
        }
      }
    }
  }
}
