{
  "openapi": "3.1.0",
  "info": {
    "title": "sendotp.email API",
    "version": "1.0.0",
    "description": "Server-to-server API for idempotent email one-time passwords. Repeat sends for the same normalized email and purpose reuse the active code during the 10-minute live window."
  },
  "servers": [
    {
      "url": "https://api.sendotp.email",
      "description": "Production"
    }
  ],
  "paths": {
    "/v1/send": {
      "post": {
        "operationId": "send-otp",
        "tags": [
          "OTP"
        ],
        "summary": "Send or resend an idempotent email code for an email and purpose.",
        "description": "Creates a live challenge when none exists, or emails the same live code again for the same normalized email and purpose. The default live window is 10 minutes.",
        "responses": {
          "200": {
            "description": "The code was accepted for delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "resent": {
                      "type": "boolean"
                    },
                    "expiresAt": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "ok",
                    "id",
                    "resent",
                    "expiresAt"
                  ],
                  "additionalProperties": false
                },
                "examples": {
                  "response1": {
                    "summary": "The code was accepted for delivery.",
                    "value": {
                      "ok": true,
                      "id": "otp_01JZ4NQ8F2T7G2A9J6P0G5QX3K",
                      "resent": false,
                      "expiresAt": 1700000600
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "The email address uses a known temporary or disposable domain.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error",
                    "message"
                  ],
                  "additionalProperties": false
                },
                "examples": {
                  "response1": {
                    "summary": "The email address uses a known temporary or disposable domain.",
                    "value": {
                      "error": "temporary_email_not_allowed",
                      "message": "Please use a permanent email address to continue."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The same live challenge was re-sent too recently. The email and purpose exceeded the account rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string"
                        },
                        "id": {
                          "type": "string"
                        },
                        "expiresAt": {
                          "type": "number"
                        },
                        "retryAfter": {
                          "type": "number"
                        }
                      },
                      "required": [
                        "error",
                        "id",
                        "expiresAt",
                        "retryAfter"
                      ],
                      "additionalProperties": false
                    },
                    {
                      "type": "object",
                      "properties": {
                        "error": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "error"
                      ],
                      "additionalProperties": false
                    }
                  ]
                },
                "examples": {
                  "response1": {
                    "summary": "The same live challenge was re-sent too recently.",
                    "value": {
                      "error": "resend_cooldown",
                      "id": "otp_01JZ4NQ8F2T7G2A9J6P0G5QX3K",
                      "expiresAt": 1700000600,
                      "retryAfter": 30
                    }
                  },
                  "response2": {
                    "summary": "The email and purpose exceeded the account rate limit.",
                    "value": {
                      "error": "rate_limited"
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Recipient email address. It is trimmed, lowercased, and validated before lookup."
                  },
                  "purpose": {
                    "type": "string",
                    "description": "Stable flow key such as login, report-access, signup, or account-delete."
                  },
                  "language": {
                    "type": "string",
                    "description": "Optional email template language. Supported: en (English), es (Spanish), fr (French), de (German), pt (Portuguese), it (Italian), and nl (Dutch). Defaults to en. Accepts base codes like es or locale tags like es-MX; unsupported languages fall back to en."
                  }
                },
                "required": [
                  "email",
                  "purpose"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/verify": {
      "post": {
        "operationId": "verify-otp",
        "tags": [
          "OTP"
        ],
        "summary": "Verify a submitted email code against the live idempotent OTP record.",
        "description": "Checks the submitted challenge id and code for the normalized email and purpose. Rejections include explicit reasons so callers can measure expired, superseded, locked, and wrong-code outcomes.",
        "responses": {
          "200": {
            "description": "The submitted code matched. The challenge was expired, superseded, locked, or the code was wrong.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "valid": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "valid"
                      ],
                      "additionalProperties": false
                    },
                    {
                      "type": "object",
                      "properties": {
                        "valid": {
                          "type": "boolean"
                        },
                        "reason": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "valid",
                        "reason"
                      ],
                      "additionalProperties": false
                    }
                  ]
                },
                "examples": {
                  "response1": {
                    "summary": "The submitted code matched.",
                    "value": {
                      "valid": true
                    }
                  },
                  "response2": {
                    "summary": "The challenge was expired, superseded, locked, or the code was wrong.",
                    "value": {
                      "valid": false,
                      "reason": "wrong_code"
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Email address originally used to request the code."
                  },
                  "purpose": {
                    "type": "string",
                    "description": "The exact purpose used when the code was sent."
                  },
                  "id": {
                    "type": "string",
                    "description": "Challenge id returned by the send endpoint."
                  },
                  "code": {
                    "type": "string",
                    "description": "Code entered by the user from their email."
                  }
                },
                "required": [
                  "email",
                  "purpose",
                  "id",
                  "code"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/test/latest-code": {
      "get": {
        "operationId": "test-latest-code",
        "tags": [
          "OTP"
        ],
        "summary": "Fetch the latest code created by a test-mode key for an email and purpose.",
        "description": "Use sk_test keys in CI to exercise the same send and verify API flow without delivering email. Test OTP records are isolated from live-key OTP records for the same account.",
        "responses": {
          "200": {
            "description": "A test-mode challenge exists for this email and purpose.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    },
                    "purpose": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    },
                    "expiresAt": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "id",
                    "email",
                    "purpose",
                    "code",
                    "expiresAt"
                  ],
                  "additionalProperties": false
                },
                "examples": {
                  "response1": {
                    "summary": "A test-mode challenge exists for this email and purpose.",
                    "value": {
                      "id": "otp_01JZ4NQ8F2T7G2A9J6P0G5QX3K",
                      "email": "customer@example.com",
                      "purpose": "report-access",
                      "code": "493021",
                      "expiresAt": 1700000600
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The request used a live key instead of a test key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                },
                "examples": {
                  "response1": {
                    "summary": "The request used a live key instead of a test key.",
                    "value": {
                      "error": "test_key_required"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No test-mode challenge has been sent for this email and purpose.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error"
                  ],
                  "additionalProperties": false
                },
                "examples": {
                  "response1": {
                    "summary": "No test-mode challenge has been sent for this email and purpose.",
                    "value": {
                      "error": "not_found"
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": true,
            "description": "Recipient email address used with the test-mode send request.",
            "schema": {
              "type": "string",
              "description": "Recipient email address used with the test-mode send request."
            }
          },
          {
            "name": "purpose",
            "in": "query",
            "required": true,
            "description": "Purpose used with the test-mode send request.",
            "schema": {
              "type": "string",
              "description": "Purpose used with the test-mode send request."
            }
          }
        ]
      }
    },
    "/v1/health": {
      "get": {
        "operationId": "health",
        "tags": [
          "OTP"
        ],
        "summary": "Check that the OTP Worker is reachable.",
        "description": "Returns a small JSON payload when the public OTP service is up.",
        "responses": {
          "200": {
            "description": "The Worker is reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false
                },
                "examples": {
                  "response1": {
                    "summary": "The Worker is reachable.",
                    "value": {
                      "ok": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key",
        "description": "Server-side SendOTP API key. Never expose this value in browser code."
      }
    }
  }
}
