Skip to main content
Webhooks allow your application to receive real-time HTTP callbacks when email events occur, such as deliveries, bounces, opens, and clicks.

How Webhooks Work

  1. You register a webhook URL with Ark
  2. When an event occurs, Ark sends an HTTP POST to your URL
  3. Your server processes the event and returns a 2xx response
  4. If delivery fails, Ark retries with exponential backoff

Creating a Webhook

from ark import Ark

client = Ark()

webhook = client.webhooks.create(
    name="My Webhook",
    url="https://yourapp.com/webhooks/ark",
    events=["MessageSent", "MessageBounced", "MessageDeliveryFailed", "MessageLinkClicked", "MessageLoaded"]
)

print(f"Webhook created: {webhook.data.id}")

Event Types

EventDescription
MessageSentEmail successfully delivered to recipient’s server
MessageDelayedTemporary delivery issue, will retry
MessageDeliveryFailedPermanent delivery failure after all retries
MessageBouncedHard bounce - recipient rejected the email
MessageHeldEmail held for manual review
MessageLoadedRecipient opened the email (tracking pixel loaded)
MessageLinkClickedRecipient clicked a link in the email
DomainDNSErrorDNS configuration issue with your sending domain

Webhook Payload

All webhooks include this structure:
{
  "event": "MessageSent",
  "timestamp": 1704672000.123456,
  "uuid": "abc123-def456-ghi789",
  "payload": {
    "message": {
      "id": 12345,
      "token": "abc123",
      "direction": "outgoing",
      "message_id": "<[email protected]>",
      "to": "[email protected]",
      "from": "[email protected]",
      "subject": "Welcome to our service",
      "timestamp": 1704672000.0,
      "spam_status": "NotSpam",
      "tag": "onboarding"
    },
    "status": "Sent",
    "details": "250 OK",
    "output": "Message accepted",
    "sent_with_ssl": true,
    "timestamp": 1704672000.123456,
    "time": 0.234
  }
}

Event-Specific Data

{
  "event": "MessageBounced",
  "timestamp": 1704672000.123456,
  "uuid": "abc123-def456-ghi789",
  "payload": {
    "original_message": {
      "id": 12345,
      "token": "abc123",
      "to": "[email protected]",
      "from": "[email protected]",
      "subject": "Welcome!"
    },
    "bounce": {
      "id": 12346,
      "token": "def456",
      "to": "[email protected]",
      "from": "[email protected]",
      "subject": "Delivery Status Notification"
    }
  }
}
{
  "event": "MessageLoaded",
  "timestamp": 1704672000.123456,
  "uuid": "abc123-def456-ghi789",
  "payload": {
    "message": {
      "id": 12345,
      "token": "abc123",
      "to": "[email protected]",
      "from": "[email protected]",
      "subject": "Welcome!"
    },
    "ip_address": "192.168.1.1",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  }
}
{
  "event": "MessageLinkClicked",
  "timestamp": 1704672000.123456,
  "uuid": "abc123-def456-ghi789",
  "payload": {
    "message": {
      "id": 12345,
      "token": "abc123",
      "to": "[email protected]",
      "from": "[email protected]",
      "subject": "Welcome!"
    },
    "url": "https://example.com/welcome",
    "token": "link-token-123",
    "ip_address": "192.168.1.1",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  }
}

Verifying Signatures

All webhooks are cryptographically signed using RSA-SHA256 for security. Each webhook request includes two headers:
HeaderDescription
X-Ark-SignatureBase64-encoded RSA-SHA256 signature of the request body
X-Ark-Signature-KIDKey ID identifying which public key was used to sign

Getting the Public Key

Ark exposes its public key via a standard JWKS (JSON Web Key Set) endpoint:
GET https://mail.arkhq.io/.well-known/jwks.json
Cache the public key in your application to avoid fetching it on every webhook. The key rarely changes, but you should refresh it if signature verification fails.

Verification Examples

import crypto from 'crypto';

// Cache this - fetch from https://mail.arkhq.io/.well-known/jwks.json
let cachedPublicKey = null;

async function getPublicKey() {
  if (cachedPublicKey) return cachedPublicKey;

  const response = await fetch('https://mail.arkhq.io/.well-known/jwks.json');
  const jwks = await response.json();

  // Convert JWK to PEM format
  const jwk = jwks.keys[0];
  cachedPublicKey = crypto.createPublicKey({ key: jwk, format: 'jwk' });
  return cachedPublicKey;
}

function verifyWebhookSignature(payload, signatureBase64, publicKey) {
  const signature = Buffer.from(signatureBase64, 'base64');
  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(payload);
  return verifier.verify(publicKey, signature);
}

// Express.js example
app.post('/webhooks/ark', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-ark-signature'];

  if (!signature) {
    return res.status(401).send('Missing signature');
  }

  try {
    const publicKey = await getPublicKey();
    const isValid = verifyWebhookSignature(req.body.toString(), signature, publicKey);

    if (!isValid) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body);

    // Acknowledge receipt immediately
    res.status(200).send('OK');

    // Process event asynchronously
    switch (event.event) {
      case 'MessageSent':
        console.log(`Email ${event.payload.message.token} sent`);
        break;
      case 'MessageBounced':
        console.log(`Email ${event.payload.original_message.token} bounced`);
        // Add to suppression list
        break;
      case 'MessageLoaded':
        console.log(`Email ${event.payload.message.token} opened`);
        break;
    }
  } catch (error) {
    console.error('Webhook verification failed:', error);
    return res.status(401).send('Verification failed');
  }
});

Retry Policy

If your endpoint doesn’t respond with a 2xx status code, Ark will retry delivery:
AttemptDelay
1Immediate
22 minutes
33 minutes
46 minutes
510 minutes
615 minutes
After 6 failed attempts, the webhook is marked as failed and won’t be retried.
Return a 2xx response quickly, then process the event asynchronously. This prevents timeouts and ensures reliable delivery.

Testing Webhooks

Use the test endpoint to verify your integration:
result = client.webhooks.test("whk_abc123", event="MessageSent")

print(f"Success: {result.data.success}")
print(f"Status code: {result.data.status_code}")
print(f"Duration: {result.data.duration}ms")
The response will include:
  • success - Whether your endpoint responded with a 2xx status
  • statusCode - The HTTP status code returned
  • body - The response body (truncated if too long)
  • duration - Response time in milliseconds

Best Practices

Never process webhooks without verifying the RSA signature first. This ensures the webhook genuinely came from Ark.
Return a 2xx within 5 seconds. Queue events for async processing if needed.
Webhooks may be delivered more than once. Use the uuid field for idempotency.
Webhook URLs must use HTTPS for security.
Fetch the JWKS once and cache it. Only refresh if signature verification fails.
Set up alerts for webhook delivery failures in your monitoring system.

Next Steps