Maildesk API
    • Maildesk Webhook
    • API Authentication
    • Subscribers
      • Get a subscriber by ID
        GET
      • Update a subscriber
        PUT
      • Delete a subscriber
        DELETE
      • Get all subscribers
        GET
      • Create a new subscriber
        POST
      • Create up to 100 subscribers in a single request
        POST
    • Tags
      • Get all tags
        GET
      • Create a new tag
        POST
      • Get a tag by ID
        GET
      • Update a tag
        PUT
      • Delete a tag
        DELETE
    • Schemas
      • Schemas
      • ContactApiResponse
      • UpdateContactAPIRequest
      • PaginatedApiResponse
      • CreateContactAPIRequest
      • PaginatedTagsApiResponse
      • BulkCreateContactAPIRequest
      • CreateTagApiRequest
      • BulkContactApiResponse
      • TagApiResponse
      • UpdateTagApiRequest
      • ContactResponseDto
      • TagResponse
      • BulkContactFailure

    Maildesk Webhook

    Maildesk Webhook Integration Guide#

    This guide shows how to receive, verify, and process Maildesk webhooks safely in your application.

    1. Understand what Maildesk sends#

    Maildesk sends webhook requests as:
    Method: POST
    Content-Type: application/json
    Success response expected from your server: 200, 201, or 202
    Maildesk signs each request with:
    X-Maildesk-Timestamp: <unix_timestamp_seconds>
    X-Maildesk-Signature: t=<unix_timestamp_seconds>,v1=<hex_hmac_sha256>
    Signature input string:
    ${timestamp}.${raw_json_body}
    HMAC algorithm:
    HMAC-SHA256
    key: your Maildesk API key secret

    2. Create a webhook endpoint in your app#

    Create an HTTPS route that accepts POST requests.
    Example endpoint:
    POST /api/webhooks/maildesk
    Important:
    Read the raw request body bytes before JSON parsing for signature verification.
    Return 200, 201, or 202 quickly after validation and enqueue heavy work asynchronously.

    3. Verify the webhook signature#

    Verification checklist:
    1.
    Read X-Maildesk-Timestamp and X-Maildesk-Signature.
    2.
    Rebuild the signed payload as "<timestamp>.<raw_body>".
    3.
    Compute HMAC-SHA256 with your Maildesk secret.
    4.
    Compare to v1 using constant-time comparison.
    5.
    Reject old timestamps (recommended: older than 5 minutes) to reduce replay risk.

    Node.js (Express) example#

    import crypto from 'crypto'
    import express from 'express'
    
    const app = express()
    
    // Keep raw bytes for exact signature verification
    app.use('/api/webhooks/maildesk', express.raw({ type: 'application/json' }))
    
    function verifyMaildeskSignature(rawBody: Buffer, timestamp: string, signatureHeader: string, secret: string) {
      const parts = Object.fromEntries(
        signatureHeader.split(',').map(kv => {
          const [k, v] = kv.split('=')
          return [k?.trim(), v?.trim()]
        }),
      )
    
      const v1 = parts.v1
      const t = parts.t
      if (!v1 || !t || t !== timestamp) return false
    
      const payload = `${timestamp}.${rawBody.toString('utf8')}`
      const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
    
      try {
        return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'))
      } catch {
        return false
      }
    }
    
    app.post('/api/webhooks/maildesk', (req, res) => {
      const timestamp = req.header('X-Maildesk-Timestamp')
      const sig = req.header('X-Maildesk-Signature')
    
      if (!timestamp || !sig) {
        return res.status(401).send('Missing Maildesk signature headers')
      }
    
      const maxSkewSeconds = 5 * 60
      const now = Math.floor(Date.now() / 1000)
      if (Math.abs(now - Number(timestamp)) > maxSkewSeconds) {
        return res.status(401).send('Stale timestamp')
      }
    
      const ok = verifyMaildeskSignature(req.body as Buffer, timestamp, sig, process.env.MAILDESK_WEBHOOK_SECRET!)
      if (!ok) {
        return res.status(401).send('Invalid signature')
      }
    
      const event = JSON.parse((req.body as Buffer).toString('utf8'))
    
      // TODO: idempotency + async processing
      // queue.publish(event)
    
      return res.status(200).send('ok')
    })

    Python (Flask) example#

    4. Implement idempotency with eventId#

    Each webhook payload includes a unique eventId.
    Use it as an idempotency key:
    1.
    Store processed eventId in durable storage.
    2.
    If the same eventId is received again, skip side effects and return success.
    This is required because Maildesk retries failed deliveries.

    5. Handle Maildesk event payloads#

    Current webhook payload format:
    {
      "type": "subscriber.confirmed",
      "eventId": "01HV...",
      "id": "contact-id",
      "firstName": "Jane",
      "lastName": "Doe",
      "email": "jane@example.com",
      "status": "subscribed",
      "createdAt": "2026-04-21T07:30:00.000Z"
    }
    Currently produced contact-related event types:
    subscriber.confirmed
    subscriber.unsubscribed
    Recommended handler pattern:
    1.
    Switch on type.
    2.
    Validate required fields.
    3.
    Queue internal processing.
    4.
    Return 200 immediately when accepted.

    6. Prepare for retries and backoff#

    If your endpoint does not return a successful response, Maildesk retries with backoff:
    1.
    5 seconds
    2.
    5 minutes
    3.
    30 minutes
    4.
    2 hours
    5.
    5 hours
    6.
    10 hours
    After max retries, the event is moved to Maildesk dead-letter flow.
    Integration implications:
    Your endpoint must be idempotent.
    Avoid long synchronous processing.
    Return 200, 201, or 202 only after basic validation and queuing.

    7. Security hardening checklist#

    Use HTTPS only.
    Verify HMAC signature for every request.
    Reject stale timestamps (for replay protection).
    Use constant-time signature comparison.
    Rotate webhook secret safely (support overlap window during rotation).
    Log eventId, type, and verification outcome (never log full secret).

    8. Test end-to-end#

    1.
    Configure your webhook URL in Maildesk settings.
    2.
    Trigger a supported event (subscriber.confirmed or subscriber.unsubscribed).
    3.
    Confirm your endpoint receives and verifies the signature.
    4.
    Confirm duplicate delivery of the same eventId is ignored safely.
    5.
    Confirm failures are retried and eventually succeed after recovery.

    9. Common integration pitfalls#

    Verifying signature on parsed/reformatted JSON instead of raw body.
    Returning 204 No Content (Maildesk success handling expects 200, 201, or 202).
    Missing idempotency guard for retried events.
    Doing heavy downstream calls before returning success.

    If you want, this can be extended with framework-specific templates (NestJS, Django, FastAPI, Laravel, Rails) using the same signing and idempotency rules.
    Modified at 2026-04-21 06:29:34
    Next
    API Authentication
    Built with