Taifa MailTaifa Mail Docs
API Reference

Webhook Events Reference

Complete reference of all webhook event types, payload formats, and signature verification for Taifa Mail.

Base URL: https://govconnect.ke/v1

When an email event occurs, Taifa Mail sends a POST request to each webhook endpoint you have registered for that event type. This page documents the webhook management API, every event type, its payload, and how to verify authenticity.

All management endpoints require authentication via API Key or JWT cookie. API keys use the tfm_k_ prefix.

Manage webhooks with an official SDK: the webhooks resource creates, lists, updates, tests, and reads deliveries. Guides: TypeScript, Python, Go, PHP, Ruby, Rust, Java, .NET, Swift.


Manage webhooks

List webhooks

GET /v1/webhooks/

Returns your active webhook endpoints as a JSON array.

curl https://govconnect.ke/v1/webhooks/ \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY"

Response:

[
  {
    "id": "8f1d2c3a-...",
    "url": "https://yourapp.com/webhooks/taifamail",
    "events": ["email.delivered", "email.bounced"],
    "secret": "whsec_...",
    "is_active": true,
    "created_at": "2026-04-06T10:00:00Z"
  }
]

Create a webhook

POST /v1/webhooks/

Request body:

FieldTypeRequiredDescription
urlstringYesThe HTTPS endpoint to deliver events to.
eventsstring[]YesEvent types to subscribe to (see Event types).

The signing secret is generated by the server and returned in the response. There is no secret field in the request body.

curl -X POST https://govconnect.ke/v1/webhooks/ \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/taifamail",
    "events": ["email.delivered"]
  }'

Returns 201 Created with the webhook object, including its secret.

Update a webhook

PATCH /v1/webhooks/{webhook_id}

Request body (all fields optional):

FieldTypeDescription
urlstringNew delivery endpoint.
eventsstring[]New event subscription list.
is_activebooleanSet false to pause delivery, true to resume.

Delete a webhook

DELETE /v1/webhooks/{webhook_id}

Returns 204 No Content.

Send a test event

POST /v1/webhooks/{webhook_id}/test

Queues a sample email.delivered event to the endpoint so you can verify your integration. The test delivery is logged like any other.

{ "queued": true, "url": "https://yourapp.com/webhooks/taifamail" }

List delivery attempts

GET /v1/webhooks/{webhook_id}/deliveries

Query parameters:

ParameterTypeDefaultDescription
pageinteger0Page number (zero-indexed).
limitinteger20Results per page (1-100).
statusstring--Filter by delivery status.

Response:

{
  "items": [
    {
      "id": "...",
      "webhook_id": "...",
      "event_type": "email.delivered",
      "status": "delivered",
      "response_status": 200,
      "attempt": 1,
      "next_retry_at": null,
      "created_at": "2026-04-06T10:00:03Z"
    }
  ],
  "total": 1,
  "page": 0,
  "limit": 20
}

Get a delivery attempt

GET /v1/webhooks/{webhook_id}/deliveries/{delivery_id}
GET /v1/webhook-deliveries/{delivery_id}

Returns the full delivery record, including the request payload, response_body, and endpoint_url. The flat /v1/webhook-deliveries/{delivery_id} form looks the delivery up by ID alone.


Common payload fields

Every webhook payload includes these fields:

FieldTypeDescription
eventstringThe event type (e.g. email.delivered).
email_idstringThe ID of the email that triggered the event.
recipientstringThe recipient email address.
timestampstring (ISO 8601)When the event occurred.
metadataobjectAdditional context (tags, domain, subject).

Event types

email.sent

The email has been handed off to the recipient's mail server.

{
  "event": "email.sent",
  "email_id": "em_abc123",
  "recipient": "jane@example.com",
  "timestamp": "2026-04-06T10:00:01Z",
  "metadata": {
    "subject": "Your order confirmation",
    "tag": "transactional",
    "domain": "notifications.yourapp.com"
  }
}

email.delivered

The recipient's mail server confirmed delivery.

{
  "event": "email.delivered",
  "email_id": "em_abc123",
  "recipient": "jane@example.com",
  "timestamp": "2026-04-06T10:00:03Z",
  "metadata": {
    "subject": "Your order confirmation",
    "tag": "transactional",
    "domain": "notifications.yourapp.com",
    "smtp_response": "250 2.0.0 OK"
  }
}

email.opened

The recipient opened the email (tracking pixel loaded).

{
  "event": "email.opened",
  "email_id": "em_abc123",
  "recipient": "jane@example.com",
  "timestamp": "2026-04-06T10:05:30Z",
  "metadata": {
    "subject": "Your order confirmation",
    "tag": "transactional",
    "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
    "ip": "41.90.xx.xx"
  }
}

email.clicked

The recipient clicked a tracked link.

{
  "event": "email.clicked",
  "email_id": "em_abc123",
  "recipient": "jane@example.com",
  "timestamp": "2026-04-06T10:06:12Z",
  "metadata": {
    "subject": "Your order confirmation",
    "tag": "transactional",
    "url": "https://yourapp.com/orders/12345",
    "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
    "ip": "41.90.xx.xx"
  }
}

email.bounced

The email was permanently rejected by the recipient's mail server.

{
  "event": "email.bounced",
  "email_id": "em_abc123",
  "recipient": "invalid@example.com",
  "timestamp": "2026-04-06T10:00:02Z",
  "metadata": {
    "subject": "Your order confirmation",
    "tag": "transactional",
    "dsn": "5.1.1",
    "response": "550 5.1.1 The email account that you tried to reach does not exist",
    "bounce_type": "hard"
  }
}

email.failed

Delivery failed due to an internal error or configuration issue.

{
  "event": "email.failed",
  "email_id": "em_abc123",
  "recipient": "jane@example.com",
  "timestamp": "2026-04-06T10:00:01Z",
  "metadata": {
    "subject": "Your order confirmation",
    "tag": "transactional",
    "error": "Domain 'unverified.com' is not verified"
  }
}

email.complained

The recipient marked the email as spam (received via feedback loop).

{
  "event": "email.complained",
  "email_id": "em_abc123",
  "recipient": "jane@example.com",
  "timestamp": "2026-04-06T14:30:00Z",
  "metadata": {
    "subject": "Your order confirmation",
    "tag": "transactional",
    "feedback_type": "abuse"
  }
}

email.unsubscribed

The recipient clicked the List-Unsubscribe link or used one-click unsubscribe.

{
  "event": "email.unsubscribed",
  "email_id": "em_abc123",
  "recipient": "jane@example.com",
  "timestamp": "2026-04-06T11:00:00Z",
  "metadata": {
    "subject": "Your order confirmation",
    "tag": "transactional",
    "method": "one-click"
  }
}

Retry schedule

If your endpoint returns a non-2xx response or times out (10-second limit), Taifa Mail retries with exponential backoff:

AttemptDelay after failure
11 minute
25 minutes
330 minutes
42 hours
56 hours
624 hours
748 hours
872 hours

After 8 failed attempts, the delivery is moved to the dead letter queue. You can view and manually replay failed deliveries from the dashboard under Settings -> Webhooks -> select webhook -> Failed Deliveries.


Signature verification

Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook's signing secret.

Always verify this signature before processing the payload.

Python

import hmac
import hashlib
 
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
 
# In your endpoint handler:
body = request.body()  # raw bytes
signature = request.headers["X-Webhook-Signature"]
if not verify_webhook(body, signature, "whsec_your_signing_secret"):
    return Response(status_code=401)

Node.js

import crypto from "crypto";
 
function verifyWebhook(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
 
// In your Express handler:
app.post("/webhooks/taifamail", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-webhook-signature"];
  if (!verifyWebhook(req.body, signature, "whsec_your_signing_secret")) {
    return res.status(401).send("Invalid signature");
  }
  const event = JSON.parse(req.body);
  // Process the event...
  res.status(200).send("OK");
});

Go

func verifyWebhook(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

Always use constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js, hmac.Equal in Go) to prevent timing attacks.

Your webhook signing secret is shown when you create the webhook. You can also find it under Settings -> Webhooks -> click the webhook -> Signing Secret.