{
  "openapi": "3.1.0",
  "info": {
    "title": "NexDeliv email API",
    "version": "1.0.0",
    "summary": "Send transactional email with one JSON call.",
    "description": "NexDeliv sends transactional email (sign-in codes, receipts, notices about a person's own account). Every request carries an API key; every answer is JSON. Bulk or unsolicited mail is not allowed. Human documentation: https://nexdeliv.com/docs.html. The same documentation as one Markdown file: https://nexdeliv.com/llms-full.txt.",
    "contact": {
      "name": "NexDeliv",
      "email": "contact@nexdeliv.com",
      "url": "https://nexdeliv.com"
    },
    "termsOfService": "https://nexdeliv.com/terms.html"
  },
  "servers": [
    {
      "url": "https://api.nexdeliv.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Documentation",
    "url": "https://nexdeliv.com/docs.html"
  },
  "security": [
    {
      "ApiKey": []
    }
  ],
  "tags": [
    {
      "name": "Email"
    }
  ],
  "paths": {
    "/v1/email": {
      "post": {
        "tags": [
          "Email"
        ],
        "operationId": "sendEmail",
        "summary": "Send one email",
        "description": "Sends one message, to up to 10 different addresses in `to`, `cc` and `bcc` together. Each address counts as one email of the plan. Only 202 means the message was accepted for sending; delivery, bounces and complaints are reported later by webhook. Retry only 429 (after the wait), 500 and 502. Never retry another 4xx as it is, and never retry a 504 without looking at the recipient.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "1 to 255 printable ASCII characters (a UUID is fine). Within 24 hours the same key with the same body returns the original 202 with the same id and `Idempotent-Replayed: true`, and nothing is sent or counted again. Use a new key for each new message.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendEmailRequest"
              },
              "examples": {
                "simple": {
                  "summary": "A sign-in code",
                  "value": {
                    "from": "Your App <hello@your-domain.com>",
                    "to": "someone@example.com",
                    "subject": "Your sign-in code",
                    "text": "Your code is 482913.",
                    "html": "<p>Your code is <strong>482913</strong>.</p>"
                  }
                },
                "template": {
                  "summary": "A stored template",
                  "value": {
                    "from": "Your App <hello@your-domain.com>",
                    "to": "someone@example.com",
                    "template": "welcome-en",
                    "vars": {
                      "name": "Arif"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted for sending. The id is the message id: every webhook event for this message carries the same messageId.",
            "headers": {
              "Idempotent-Replayed": {
                "description": "`true` when this is the stored answer of an earlier request with the same Idempotency-Key.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendEmailResponse"
                },
                "examples": {
                  "accepted": {
                    "value": {
                      "id": "010e01a17b5ad26e-10055656-9b2f-4b83-8369-48e48a22c42c-000000"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request is wrong. Fix it; do not retry it as it is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_body": {
                    "value": {
                      "error": "invalid_body"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, unknown or disabled (all look the same).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unauthorized": {
                    "value": {
                      "error": "unauthorized"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "The plan or the prepaid emails do not allow sending now. Pay or buy emails in the dashboard.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "credit_empty": {
                    "value": {
                      "error": "credit_empty"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "from_not_allowed: the sender is not on a verified domain of the account. attachments_not_allowed: the account has no plan with attachments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "from_not_allowed": {
                    "value": {
                      "error": "from_not_allowed"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The template named in `template` does not exist in the account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "template_not_found": {
                    "value": {
                      "error": "template_not_found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The first request with this Idempotency-Key has not finished. Wait a second (Retry-After: 1) and send the same request again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "idempotency_in_progress": {
                    "value": {
                      "error": "idempotency_in_progress"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The request is understood but refused: an address is suppressed, mistyped or unsubscribed, or the Idempotency-Key was used with another body. Nothing is counted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "recipient_suppressed": {
                    "value": {
                      "error": "recipient_suppressed"
                    }
                  }
                }
              }
            }
          },
          "423": {
            "description": "Sending is paused on the account because its bounces or complaints are high. Do not retry in a loop; clean the list and write to contact@nexdeliv.com.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "sending_held": {
                    "value": {
                      "error": "sending_held"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "A limit was reached (rate, month, day, shared capacity of the Free plan, attachment volume). Wait for Retry-After, or the next window, and send again.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the next window (1 to 5), or until 00:00 UTC for free_capacity.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "rate_limited": {
                    "value": {
                      "error": "rate_limited"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Our side. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "internal": {
                    "value": {
                      "error": "internal"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "The email provider refused or failed; nothing was sent and the send is given back. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "send_failed": {
                    "value": {
                      "error": "send_failed"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "unsubscribe is true but the unsubscribe links are not set up on our side. Write to us.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unsubscribe_not_set_up": {
                    "value": {
                      "error": "unsubscribe_not_set_up"
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "The message may have been accepted and the answer was lost. Do NOT send it again without looking at the recipient; the same Idempotency-Key answers the same 504.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "send_unconfirmed": {
                    "value": {
                      "error": "send_unconfirmed"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "emailEvent": {
      "post": {
        "tags": [
          "Email"
        ],
        "operationId": "receiveEmailEvent",
        "summary": "An event about an email you sent",
        "description": "NexDeliv calls the https address you set under Webhook in the dashboard. Headers: `NexDeliv-Event` (the type), `NexDeliv-Event-Id` and `NexDeliv-Signature` (`t=<unix seconds>,v1=<hex>`). Verify it: HMAC-SHA256 over `<t>.<raw body>` with your whsec_ secret as the key, written in hex, compared with v1 in constant time; refuse a `t` more than five minutes from your clock. Use the raw body exactly as it arrived. Answer with any 2xx within a few seconds. A timeout, 408, 429 or 5xx is retried a few minutes later, up to three attempts; any other answer is final. Delivery is at least once and out of order is possible: keep the ids you handled. Redirects are not followed.",
        "security": [],
        "parameters": [
          {
            "name": "NexDeliv-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "NexDeliv-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "evt_9f2c1d7a4b6e8f0a1b2c3d4e"
              ]
            }
          },
          {
            "name": "NexDeliv-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "t=1761559202,v1=5b1f0c6a8e..."
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Any 2xx status means the event was received."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "nxd_...",
        "description": "The API key of the account, made in the dashboard (shown once). Keep it on your server in a secret such as the NEXDELIV_KEY environment variable; never in a web page, a mobile app or a public repository."
      }
    },
    "schemas": {
      "SendEmailRequest": {
        "type": "object",
        "required": [
          "subject",
          "text",
          "to"
        ],
        "description": "`to`, `subject` and `text` are required (unless `template` is used, which replaces `subject`, `text` and `html`). Every message needs `text`, even when it also has `html`. A message sent without a template that has a `{{...}}` in its subject, text or html is refused with 400 unfilled_placeholder, unless `raw` is true.",
        "properties": {
          "to": {
            "description": "The recipient, or a list. Up to 10 different addresses in `to`, `cc` and `bcc` together.",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 254,
                "examples": [
                  "someone@example.com"
                ]
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 254
                },
                "maxItems": 10
              }
            ]
          },
          "cc": {
            "description": "Shown to every recipient.",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 254,
                "examples": [
                  "someone@example.com"
                ]
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 254
                }
              }
            ]
          },
          "bcc": {
            "description": "Hidden from the others.",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 254,
                "examples": [
                  "someone@example.com"
                ]
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 254
                }
              }
            ]
          },
          "subject": {
            "type": "string",
            "maxLength": 200,
            "description": "One line (no line breaks)."
          },
          "text": {
            "type": "string",
            "maxLength": 20000,
            "description": "The plain-text body."
          },
          "html": {
            "type": "string",
            "maxLength": 100000,
            "description": "An HTML version."
          },
          "from": {
            "type": "string",
            "description": "`address@domain` or `Name <address@domain>`, on a verified domain of the account. The display name is up to 80 characters. When left out, the account's default sender is used."
          },
          "replyTo": {
            "description": "Where replies go (up to 3).",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 254,
                "examples": [
                  "someone@example.com"
                ]
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 254
                },
                "maxItems": 3
              }
            ]
          },
          "headers": {
            "type": "object",
            "description": "Extra headers, up to 10; a name of letters, digits and hyphens, a value of printable ASCII up to 500 characters. Headers the message sets itself (From, To, Cc, Bcc, Reply-To, Subject, Date, Message-ID, Content-Type, MIME-Version, Return-Path ...) and any header that the delivery system adds itself, or that starts with X-NexDeliv-, are refused (400 invalid_headers).",
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "maxProperties": 10
          },
          "attachments": {
            "type": "array",
            "maxItems": 5,
            "description": "Files, on a plan that has them (paid plans; not the Free plan or the free trial). Up to 5 files and 4,000,000 bytes together, before base64. Programs and scripts (.exe, .bat, .js, .vbs, .msi, .ps1 ...) are refused.",
            "items": {
              "type": "object",
              "required": [
                "filename",
                "content"
              ],
              "properties": {
                "filename": {
                  "type": "string",
                  "maxLength": 255,
                  "description": "With its extension; no / \\ : * ? \" < > | and no line break.",
                  "examples": [
                    "receipt.pdf"
                  ]
                },
                "content": {
                  "type": "string",
                  "contentEncoding": "base64",
                  "description": "The file in standard base64. Not empty."
                },
                "contentType": {
                  "type": "string",
                  "description": "Optional; follows from the extension when left out.",
                  "examples": [
                    "application/pdf"
                  ]
                }
              }
            }
          },
          "template": {
            "type": "string",
            "pattern": "^[a-z0-9-]{1,64}$",
            "description": "The name of a stored template, instead of subject, text and html. Made in the dashboard."
          },
          "vars": {
            "type": "object",
            "description": "The values for the {{variables}} of the template: a text, a number or true/false; up to 50 variables, a value of up to 2,000 characters. Every variable the template uses must be here (400 missing_variable). In html a value is escaped.",
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean"
              ]
            },
            "maxProperties": 50
          },
          "unsubscribe": {
            "type": "boolean",
            "description": "`true` makes a message that can be stopped: one recipient only, a List-Unsubscribe header (one click) and `{{unsubscribe_url}}` filled in text and html. Do not set List-Unsubscribe yourself with it."
          },
          "track": {
            "type": "object",
            "description": "Ask for opens and clicks to be told to your webhook. Off unless asked. Needs html and one recipient (400 invalid_track). Do not use it on sign-in codes or password resets.",
            "properties": {
              "opens": {
                "type": "boolean"
              },
              "clicks": {
                "type": "boolean"
              }
            },
            "additionalProperties": false
          },
          "raw": {
            "type": "boolean",
            "description": "`true` says that a {{...}} in a message without a template is meant."
          }
        }
      },
      "SendEmailResponse": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The message id."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Clients should tolerate a code that is not listed: new codes can be added.",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_body",
              "invalid_to",
              "invalid_cc",
              "invalid_bcc",
              "too_many_recipients",
              "invalid_subject",
              "invalid_text",
              "invalid_html",
              "invalid_reply_to",
              "invalid_headers",
              "invalid_from",
              "invalid_attachments",
              "attachment_type_not_allowed",
              "attachments_too_large",
              "invalid_unsubscribe",
              "invalid_track",
              "invalid_template_use",
              "invalid_vars",
              "missing_variable",
              "unfilled_placeholder",
              "invalid_idempotency_key",
              "unauthorized",
              "credit_empty",
              "plan_expired",
              "plan_canceled",
              "from_not_allowed",
              "attachments_not_allowed",
              "template_not_found",
              "idempotency_in_progress",
              "recipient_suppressed",
              "recipient_typo",
              "recipient_invalid_domain",
              "recipient_unsubscribed",
              "idempotency_key_reused",
              "sending_held",
              "rate_limited",
              "busy",
              "monthly_limit",
              "daily_limit",
              "free_capacity",
              "attachments_limit",
              "internal",
              "send_failed",
              "unsubscribe_not_set_up",
              "send_unconfirmed"
            ]
          },
          "address": {
            "type": "string",
            "description": "recipient_typo and recipient_invalid_domain: the address concerned."
          },
          "suggestion": {
            "type": "string",
            "description": "recipient_typo: the address that was probably meant, when it can be told."
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ],
        "description": "The body never contains the subject or the content of your message.",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "evt_9f2c1d7a4b6e8f0a1b2c3d4e"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "email.delivered",
              "email.bounced",
              "email.complained",
              "email.rejected",
              "email.unsubscribed",
              "email.opened",
              "email.clicked"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the event happened (UTC)."
          },
          "data": {
            "type": "object",
            "required": [
              "messageId",
              "to"
            ],
            "properties": {
              "messageId": {
                "type": "string",
                "description": "The id answered by POST /v1/email (empty for email.unsubscribed)."
              },
              "to": {
                "type": "string"
              },
              "bounce": {
                "type": "object",
                "description": "email.bounced. `Permanent` means the address does not work and is now suppressed; `Transient` means it might work next time.",
                "properties": {
                  "type": {
                    "type": "string",
                    "examples": [
                      "Permanent",
                      "Transient"
                    ]
                  },
                  "subType": {
                    "type": "string"
                  }
                }
              },
              "complaint": {
                "type": "object",
                "description": "email.complained. `type` is what the mailbox provider reported (abuse, fraud ...) and can be missing.",
                "properties": {
                  "type": {
                    "type": "string"
                  }
                }
              },
              "reject": {
                "type": "object",
                "description": "email.rejected: we refused to send it.",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "open": {
                "type": "object",
                "description": "email.opened. An open is a hint, not a count.",
                "properties": {
                  "bot": {
                    "type": "boolean",
                    "description": "true when we think a program opened it."
                  }
                }
              },
              "click": {
                "type": "object",
                "description": "email.clicked.",
                "properties": {
                  "link": {
                    "type": "string",
                    "description": "The address as you wrote it."
                  },
                  "bot": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
