openapi: 3.0.3 info: title: happyDeliver API description: Email Deliverability Testing Platform API version: 0.1.0 contact: name: happyDomain team url: https://github.com/happyDomain/happydeliver email: contact+api@happydomain.org license: name: GNU Affero General Public License v3.0 or later url: https://spdx.org/licenses/AGPL-3.0-or-later.html servers: - url: http://localhost:8080/api description: Local development server - url: https://api.example.com/api description: Production server tags: - name: tests description: Test management operations - name: reports description: Report retrieval operations - name: health description: Service health and status paths: /test: post: tags: - tests summary: Create a new deliverability test description: Generates a unique test email address for sending test emails. No database record is created until an email is received. operationId: createTest responses: '201': description: Test email address generated successfully content: application/json: schema: $ref: '#/components/schemas/TestResponse' '500': description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' /test/{id}: get: tags: - tests summary: Get test status description: Check if a report exists for the given test ID (base32-encoded). Returns pending if no report exists, analyzed if a report is available. operationId: getTest parameters: - name: id in: path required: true schema: type: string pattern: '^[a-z0-9-]+$' description: Base32-encoded test ID (with hyphens) responses: '200': description: Test status retrieved successfully content: application/json: schema: $ref: '#/components/schemas/Test' '500': description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' /report/{id}: get: tags: - reports summary: Get detailed report description: Retrieve comprehensive deliverability analysis report operationId: getReport parameters: - name: id in: path required: true schema: type: string pattern: '^[a-z0-9-]+$' description: Base32-encoded test ID (with hyphens) responses: '200': description: Report retrieved successfully content: application/json: schema: $ref: '#/components/schemas/Report' '404': description: Report not found content: application/json: schema: $ref: '#/components/schemas/Error' /report/{id}/raw: get: tags: - reports summary: Get raw annotated email description: Retrieve the original email with headers added by filters operationId: getRawEmail parameters: - name: id in: path required: true schema: type: string pattern: '^[a-z0-9-]+$' description: Base32-encoded test ID (with hyphens) responses: '200': description: Raw email retrieved successfully content: text/plain: schema: type: string '404': description: Email not found content: application/json: schema: $ref: '#/components/schemas/Error' /report/{id}/reanalyze: post: tags: - reports summary: Reanalyze email and regenerate report description: Re-run the analysis on the stored raw email to regenerate the report with the latest analyzer version. This is useful after analyzer improvements or bug fixes. operationId: reanalyzeReport parameters: - name: id in: path required: true schema: type: string pattern: '^[a-z0-9-]+$' description: Base32-encoded test ID (with hyphens) responses: '200': description: Report regenerated successfully content: application/json: schema: $ref: '#/components/schemas/Report' '404': description: Email not found content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal server error during reanalysis content: application/json: schema: $ref: '#/components/schemas/Error' /domain: post: tags: - tests summary: Test a domain's email configuration description: Analyzes DNS records (MX, SPF, DMARC, BIMI) for a domain without requiring an actual email to be sent. Returns results immediately. operationId: testDomain requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DomainTestRequest' responses: '200': description: Domain test completed successfully content: application/json: schema: $ref: '#/components/schemas/DomainTestResponse' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' /blacklist: post: tags: - tests summary: Check an IP address against DNS blacklists description: Tests a single IP address (IPv4 or IPv6) against configured DNS-based blacklists (RBLs) without requiring an actual email to be sent. Returns results immediately. operationId: checkBlacklist requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BlacklistCheckRequest' responses: '200': description: Blacklist check completed successfully content: application/json: schema: $ref: '#/components/schemas/BlacklistCheckResponse' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/Error' '500': description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' /status: get: tags: - health summary: Service health check description: Get service health status and component versions operationId: getStatus responses: '200': description: Service status content: application/json: schema: $ref: '#/components/schemas/Status' components: schemas: Test: type: object required: - id - email - status properties: id: type: string pattern: '^[a-z0-9-]+$' description: Unique test identifier (base32-encoded with hyphens) example: "krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a" email: type: string format: email description: Unique test email address example: "test-krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a@example.com" status: type: string enum: [pending, analyzed] description: Current test status (pending = no report yet, analyzed = report available) example: "analyzed" TestResponse: type: object required: - id - email - status properties: id: type: string pattern: '^[a-z0-9-]+$' description: Unique test identifier (base32-encoded with hyphens) example: "krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a" email: type: string format: email example: "test-krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a@example.com" status: type: string enum: [pending] example: "pending" message: type: string example: "Send your test email to the address above" Report: type: object required: - id - test_id - score - grade - created_at properties: id: type: string pattern: '^[a-z0-9-]+$' description: Report identifier (base32-encoded with hyphens) test_id: type: string pattern: '^[a-z0-9-]+$' description: Associated test ID (base32-encoded with hyphens) score: type: integer minimum: 0 maximum: 100 description: Overall deliverability score as percentage (0-100) example: 85 grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score (A+ is best, F is worst) example: "A" summary: $ref: '#/components/schemas/ScoreSummary' authentication: $ref: '#/components/schemas/AuthenticationResults' spamassassin: $ref: '#/components/schemas/SpamAssassinResult' dns_results: $ref: '#/components/schemas/DNSResults' blacklists: type: object additionalProperties: type: array items: $ref: '#/components/schemas/BlacklistCheck' description: Map of IP addresses to their blacklist check results (array of checks per IP) example: "192.0.2.1": - rbl: "zen.spamhaus.org" listed: false - rbl: "bl.spamcop.net" listed: false content_analysis: $ref: '#/components/schemas/ContentAnalysis' header_analysis: $ref: '#/components/schemas/HeaderAnalysis' raw_headers: type: string description: Raw email headers created_at: type: string format: date-time ScoreSummary: type: object required: - dns_score - dns_grade - authentication_score - authentication_grade - spam_score - spam_grade - blacklist_score - blacklist_grade - header_score - header_grade - content_score - content_grade properties: dns_score: type: integer minimum: 0 maximum: 100 description: DNS records score (in percentage) example: 42 dns_grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score (A+ is best, F is worst) example: "A" authentication_score: type: integer minimum: 0 maximum: 100 description: SPF/DKIM/DMARC score (in percentage) example: 28 authentication_grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score (A+ is best, F is worst) example: "A" spam_score: type: integer minimum: 0 maximum: 100 description: SpamAssassin score (in percentage) example: 15 spam_grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score (A+ is best, F is worst) example: "A" blacklist_score: type: integer minimum: 0 maximum: 100 description: Blacklist check score (in percentage) example: 20 blacklist_grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score (A+ is best, F is worst) example: "A" header_score: type: integer minimum: 0 maximum: 100 description: Header quality score (in percentage) example: 9 header_grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score (A+ is best, F is worst) example: "A" content_score: type: integer minimum: 0 maximum: 100 description: Content quality score (in percentage) example: 18 content_grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score (A+ is best, F is worst) example: "A" ContentAnalysis: type: object properties: has_html: type: boolean description: Whether email contains HTML part example: true has_plaintext: type: boolean description: Whether email contains plaintext part example: true html_issues: type: array items: $ref: '#/components/schemas/ContentIssue' description: Issues found in HTML content links: type: array items: $ref: '#/components/schemas/LinkCheck' description: Analysis of links found in the email images: type: array items: $ref: '#/components/schemas/ImageCheck' description: Analysis of images in the email text_to_image_ratio: type: number format: float description: Ratio of text to images (higher is better) example: 0.75 has_unsubscribe_link: type: boolean description: Whether email contains an unsubscribe link example: true unsubscribe_methods: type: array items: type: string enum: [link, mailto, list-unsubscribe-header, one-click] description: Available unsubscribe methods example: ["link", "list-unsubscribe-header"] ContentIssue: type: object required: - type - severity - message properties: type: type: string enum: [broken_html, missing_alt, excessive_images, obfuscated_url, suspicious_link, dangerous_html] description: Type of content issue example: "missing_alt" severity: type: string enum: [critical, high, medium, low, info] description: Issue severity example: "medium" message: type: string description: Human-readable description example: "3 images are missing alt attributes" location: type: string description: Where the issue was found example: "HTML body line 42" advice: type: string description: How to fix this issue example: "Add descriptive alt text to all images for better accessibility and deliverability" LinkCheck: type: object required: - url - status properties: url: type: string format: uri description: The URL found in the email example: "https://example.com/page" status: type: string enum: [valid, broken, suspicious, redirected, timeout] description: Link validation status example: "valid" http_code: type: integer description: HTTP status code received example: 200 redirect_chain: type: array items: type: string description: URLs in the redirect chain, if any example: ["https://example.com", "https://www.example.com"] is_shortened: type: boolean description: Whether this is a URL shortener example: false ImageCheck: type: object required: - has_alt properties: src: type: string description: Image source URL or path example: "https://example.com/logo.png" has_alt: type: boolean description: Whether image has alt attribute example: true alt_text: type: string description: Alt text content example: "Company Logo" is_tracking_pixel: type: boolean description: Whether this appears to be a tracking pixel (1x1 image) example: false HeaderAnalysis: type: object properties: has_mime_structure: type: boolean description: Whether body has a MIME structure example: true headers: type: object additionalProperties: $ref: '#/components/schemas/HeaderCheck' description: Map of header names to their check results (e.g., "from", "to", "dkim-signature") example: from: present: true value: "sender@example.com" valid: true importance: "required" date: present: true value: "Mon, 1 Jan 2024 12:00:00 +0000" valid: true importance: "required" received_chain: type: array items: $ref: '#/components/schemas/ReceivedHop' description: Chain of Received headers showing email path domain_alignment: $ref: '#/components/schemas/DomainAlignment' issues: type: array items: $ref: '#/components/schemas/HeaderIssue' description: Issues found in headers HeaderCheck: type: object required: - present properties: present: type: boolean description: Whether the header is present example: true value: type: string description: Header value example: "sender@example.com" valid: type: boolean description: Whether the value is valid/well-formed example: true importance: type: string enum: [required, recommended, optional, newsletter] description: How important this header is for deliverability example: "required" issues: type: array items: type: string description: Any issues with this header example: ["Invalid date format"] ReceivedHop: type: object properties: from: type: string description: Sending server hostname example: "mail.example.com" by: type: string description: Receiving server hostname example: "mx.receiver.com" with: type: string description: Protocol used example: "ESMTPS" id: type: string description: Message ID at this hop timestamp: type: string format: date-time description: When this hop occurred ip: type: string description: IP address of the sending server (IPv4 or IPv6) example: "192.0.2.1" reverse: type: string description: Reverse DNS (PTR record) for the IP address example: "mail.example.com" DKIMDomainInfo: type: object required: - domain - org_domain properties: domain: type: string description: DKIM signature domain example: "mail.example.com" org_domain: type: string description: Organizational domain extracted from DKIM domain (using Public Suffix List) example: "example.com" DomainAlignment: type: object properties: from_domain: type: string description: Domain from From header example: "example.com" from_org_domain: type: string description: Organizational domain extracted from From header (using Public Suffix List) example: "example.com" return_path_domain: type: string description: Domain from Return-Path header example: "example.com" return_path_org_domain: type: string description: Organizational domain extracted from Return-Path header (using Public Suffix List) example: "example.com" dkim_domains: type: array items: $ref: '#/components/schemas/DKIMDomainInfo' description: Domains from DKIM signatures with their organizational domains aligned: type: boolean description: Whether all domains align (strict alignment - exact match) example: true relaxed_aligned: type: boolean description: Whether domains satisfy relaxed alignment (organizational domain match) example: true issues: type: array items: type: string description: Alignment issues example: ["Return-Path domain does not match From domain"] HeaderIssue: type: object required: - header - severity - message properties: header: type: string description: Header name example: "Date" severity: type: string enum: [critical, high, medium, low, info] description: Issue severity example: "medium" message: type: string description: Human-readable description example: "Date header is in the future" advice: type: string description: How to fix this issue example: "Ensure your mail server clock is synchronized with NTP" AuthenticationResults: type: object properties: spf: $ref: '#/components/schemas/AuthResult' dkim: type: array items: $ref: '#/components/schemas/AuthResult' dmarc: $ref: '#/components/schemas/AuthResult' bimi: $ref: '#/components/schemas/AuthResult' arc: $ref: '#/components/schemas/ARCResult' iprev: $ref: '#/components/schemas/IPRevResult' x_google_dkim: $ref: '#/components/schemas/AuthResult' description: Google-specific DKIM authentication result (x-google-dkim) x_aligned_from: $ref: '#/components/schemas/AuthResult' description: X-Aligned-From authentication result (checks address alignment) AuthResult: type: object required: - result properties: result: type: string enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass] description: Authentication result example: "pass" domain: type: string description: Domain being authenticated example: "example.com" selector: type: string description: DKIM selector (for DKIM only) example: "default" details: type: string description: Additional details about the result ARCResult: type: object required: - result properties: result: type: string enum: [pass, fail, none] description: Overall ARC chain validation result example: "pass" chain_valid: type: boolean description: Whether the ARC chain signatures are valid example: true chain_length: type: integer description: Number of ARC sets in the chain example: 2 details: type: string description: Additional details about ARC validation example: "ARC chain valid with 2 intermediaries" IPRevResult: type: object required: - result properties: result: type: string enum: [pass, fail, temperror, permerror] description: IP reverse DNS lookup result example: "pass" ip: type: string description: IP address that was checked example: "195.110.101.58" hostname: type: string description: Hostname from reverse DNS lookup (PTR record) example: "authsmtp74.register.it" details: type: string description: Additional details about the IP reverse lookup example: "smtp.remote-ip=195.110.101.58 (authsmtp74.register.it)" SpamAssassinResult: type: object required: - score - required_score - is_spam - test_details properties: version: type: string description: SpamAssassin version example: "SpamAssassin 4.0.1" score: type: number format: float description: SpamAssassin spam score example: 2.3 required_score: type: number format: float description: Threshold for spam classification example: 5.0 is_spam: type: boolean description: Whether message is classified as spam example: false tests: type: array items: type: string description: List of triggered SpamAssassin tests example: ["BAYES_00", "DKIM_SIGNED"] test_details: type: object additionalProperties: $ref: '#/components/schemas/SpamTestDetail' description: Map of test names to their detailed results example: BAYES_00: name: "BAYES_00" score: -1.9 description: "Bayes spam probability is 0 to 1%" DKIM_SIGNED: name: "DKIM_SIGNED" score: 0.1 description: "Message has a DKIM or DK signature, not necessarily valid" report: type: string description: Full SpamAssassin report SpamTestDetail: type: object required: - name - score properties: name: type: string description: Test name example: "BAYES_00" score: type: number format: float description: Score contribution of this test example: -1.9 description: type: string description: Human-readable description of what this test checks example: "Bayes spam probability is 0 to 1%" DNSResults: type: object required: - from_domain properties: from_domain: type: string description: From Domain name example: "example.com" rp_domain: type: string description: Return Path Domain name example: "example.com" from_mx_records: type: array items: $ref: '#/components/schemas/MXRecord' description: MX records for the From domain rp_mx_records: type: array items: $ref: '#/components/schemas/MXRecord' description: MX records for the Return-Path domain spf_records: type: array items: $ref: '#/components/schemas/SPFRecord' description: SPF records found (includes resolved include directives) dkim_records: type: array items: $ref: '#/components/schemas/DKIMRecord' description: DKIM records found dmarc_record: $ref: '#/components/schemas/DMARCRecord' bimi_record: $ref: '#/components/schemas/BIMIRecord' dnssec_enabled: type: boolean description: Whether the From domain has DNSSEC enabled with valid chain of trust example: true ptr_records: type: array items: type: string description: PTR (reverse DNS) records for the sender IP address example: ["mail.example.com", "smtp.example.com"] ptr_forward_records: type: array items: type: string description: A or AAAA records resolved from the PTR hostnames (forward confirmation) example: ["192.0.2.1", "2001:db8::1"] errors: type: array items: type: string description: DNS lookup errors MXRecord: type: object required: - host - priority - valid properties: host: type: string description: MX hostname example: "mail.example.com" priority: type: integer format: uint16 description: MX priority (lower is higher priority) example: 10 valid: type: boolean description: Whether the MX record is valid example: true error: type: string description: Error message if validation failed example: "Failed to lookup MX records" SPFRecord: type: object required: - valid properties: domain: type: string description: Domain this SPF record belongs to example: "example.com" record: type: string description: SPF record content example: "v=spf1 include:_spf.example.com ~all" valid: type: boolean description: Whether the SPF record is valid example: true all_qualifier: type: string enum: ["+", "-", "~", "?"] description: "Qualifier for the 'all' mechanism: + (pass), - (fail), ~ (softfail), ? (neutral)" example: "~" error: type: string description: Error message if validation failed example: "No SPF record found" DKIMRecord: type: object required: - selector - domain - valid properties: selector: type: string description: DKIM selector example: "default" domain: type: string description: Domain name example: "example.com" record: type: string description: DKIM record content example: "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..." valid: type: boolean description: Whether the DKIM record is valid example: true error: type: string description: Error message if validation failed example: "No DKIM record found" DMARCRecord: type: object required: - valid properties: record: type: string description: DMARC record content example: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com" policy: type: string enum: [none, quarantine, reject, unknown] description: DMARC policy example: "quarantine" subdomain_policy: type: string enum: [none, quarantine, reject, unknown] description: DMARC subdomain policy (sp tag) - policy for subdomains if different from main policy example: "quarantine" percentage: type: integer minimum: 0 maximum: 100 description: Percentage of messages subjected to filtering (pct tag, default 100) example: 100 spf_alignment: type: string enum: [relaxed, strict] description: SPF alignment mode (aspf tag) example: "relaxed" dkim_alignment: type: string enum: [relaxed, strict] description: DKIM alignment mode (adkim tag) example: "relaxed" valid: type: boolean description: Whether the DMARC record is valid example: true error: type: string description: Error message if validation failed example: "No DMARC record found" BIMIRecord: type: object required: - selector - domain - valid properties: selector: type: string description: BIMI selector example: "default" domain: type: string description: Domain name example: "example.com" record: type: string description: BIMI record content example: "v=BIMI1; l=https://example.com/logo.svg" logo_url: type: string format: uri description: URL to the brand logo (SVG) example: "https://example.com/logo.svg" vmc_url: type: string format: uri description: URL to Verified Mark Certificate (optional) example: "https://example.com/vmc.pem" valid: type: boolean description: Whether the BIMI record is valid example: true error: type: string description: Error message if validation failed example: "No BIMI record found" BlacklistCheck: type: object required: - rbl - listed properties: rbl: type: string description: RBL/DNSBL name example: "zen.spamhaus.org" listed: type: boolean description: Whether IP is listed example: false response: type: string description: RBL response code or message example: "127.0.0.2" error: type: string description: RBL error if any Status: type: object required: - status - version properties: status: type: string enum: [healthy, degraded, unhealthy] description: Overall service status example: "healthy" version: type: string description: Service version example: "0.1.0-dev" components: type: object properties: database: type: string enum: [up, down] example: "up" mta: type: string enum: [up, down] example: "up" uptime: type: integer description: Service uptime in seconds example: 3600 Error: type: object required: - error - message properties: error: type: string description: Error code example: "not_found" message: type: string description: Human-readable error message example: "Test not found" details: type: string description: Additional error details DomainTestRequest: type: object required: - domain properties: domain: type: string pattern: '^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' description: Domain name to test (e.g., example.com) example: "example.com" DomainTestResponse: type: object required: - domain - score - grade - dns_results properties: domain: type: string description: The tested domain name example: "example.com" score: type: integer minimum: 0 maximum: 100 description: Overall domain configuration score (0-100) example: 85 grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score example: "A" dns_results: $ref: '#/components/schemas/DNSResults' BlacklistCheckRequest: type: object required: - ip properties: ip: type: string description: IPv4 or IPv6 address to check against blacklists example: "192.0.2.1" pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$|^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,6}:([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$' BlacklistCheckResponse: type: object required: - ip - checks - listed_count - score - grade properties: ip: type: string description: The IP address that was checked example: "192.0.2.1" checks: type: array items: $ref: '#/components/schemas/BlacklistCheck' description: List of blacklist check results listed_count: type: integer description: Number of blacklists that have this IP listed example: 0 score: type: integer minimum: 0 maximum: 100 description: Blacklist score (0-100, higher is better) example: 100 grade: type: string enum: [A+, A, B, C, D, E, F] description: Letter grade representation of the score example: "A+"