Taifa MailTaifa Mail Docs
API Reference

Contacts API

API reference for managing contact lists, contacts, CSV imports, and bulk sending in Taifa Mail.

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

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

Using an official SDK? The contacts resource wraps these endpoints. Guides: TypeScript, Python, Go, PHP, Ruby, Rust, Java, .NET, Swift.


Create a contact list

POST /v1/contacts/

Creates a new contact list for organising your recipients.

Request body:

{
  "name": "Newsletter subscribers",
  "description": "Users who signed up via the website footer form",
  "icon_seed": "newsletter"
}
FieldTypeRequiredDescription
namestringYesName of the contact list.
descriptionstringNoDescription of the list's purpose.
icon_seedstringNoSeed string used to generate a unique list icon in the dashboard.

Response (201 Created):

{
  "id": "cl_abc123",
  "name": "Newsletter subscribers",
  "description": "Users who signed up via the website footer form",
  "icon_seed": "newsletter",
  "contact_count": 0,
  "created_at": "2026-04-10T08:00:00Z"
}
curl -X POST https://govconnect.ke/v1/contacts/ \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Newsletter subscribers",
    "description": "Users who signed up via the website footer form"
  }'

List contact lists

GET /v1/contacts/

Returns all contact lists for the authenticated workspace as a JSON array.

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

Response:

[
  {
    "id": "cl_abc123",
    "name": "Newsletter subscribers",
    "description": "Users who signed up via the website footer form",
    "icon_seed": "newsletter",
    "contact_count": 1250,
    "created_at": "2026-04-10T08:00:00Z"
  },
  {
    "id": "cl_def456",
    "name": "Product updates",
    "description": null,
    "icon_seed": null,
    "contact_count": 340,
    "created_at": "2026-04-08T12:00:00Z"
  }
]

Get a contact list

GET /v1/contacts/{list_id}

Returns a contact list with its contacts, paginated.

Query parameters:

ParameterTypeDefaultDescription
pageinteger0Page number (zero-indexed).
limitinteger50Contacts per page (1-200).
curl "https://govconnect.ke/v1/contacts/cl_abc123?page=0&limit=20" \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY"

Response:

{
  "id": "cl_abc123",
  "name": "Newsletter subscribers",
  "description": "Users who signed up via the website footer form",
  "icon_seed": "newsletter",
  "contact_count": 1250,
  "created_at": "2026-04-10T08:00:00Z",
  "contacts": [
    {
      "id": "ct_abc123",
      "email": "alice@example.com",
      "name": "Alice Kamau",
      "metadata": {"city": "Nairobi", "plan": "pro"},
      "created_at": "2026-04-10T09:00:00Z"
    },
    {
      "id": "ct_def456",
      "email": "bob@example.com",
      "name": "Bob Otieno",
      "metadata": null,
      "created_at": "2026-04-10T09:01:00Z"
    }
  ]
}

Update a contact list

PATCH /v1/contacts/{list_id}

Updates a contact list's name, description, or icon seed. Only include the fields you want to change.

Request body:

{
  "name": "Weekly newsletter",
  "description": "Updated description"
}
FieldTypeRequiredDescription
namestringNoNew name for the list.
descriptionstringNoNew description.
icon_seedstringNoNew icon seed.

cURL

curl -X PATCH https://govconnect.ke/v1/contacts/cl_abc123 \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Weekly newsletter"}'

Python

import requests
 
response = requests.patch(
    "https://govconnect.ke/v1/contacts/cl_abc123",
    headers={"Authorization": "Bearer tfm_k_YOUR_API_KEY"},
    json={"name": "Weekly newsletter"}
)
 
print(response.json())

Node.js

const response = await fetch("https://govconnect.ke/v1/contacts/cl_abc123", {
  method: "PATCH",
  headers: {
    "Authorization": "Bearer tfm_k_YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Weekly newsletter" }),
});
 
const data = await response.json();
console.log(data);

Delete a contact list

DELETE /v1/contacts/{list_id}

Permanently deletes a contact list and all its contacts.

curl -X DELETE https://govconnect.ke/v1/contacts/cl_abc123 \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY"

Response (204 No Content):

No response body.

Deleting a contact list permanently removes all contacts in the list. This action cannot be undone.


Add a contact to a list

POST /v1/contacts/{list_id}/contacts

Adds a single contact to a list.

Request body:

{
  "email": "alice@example.com",
  "name": "Alice Kamau",
  "metadata": {
    "city": "Nairobi",
    "plan": "pro"
  }
}
FieldTypeRequiredDescription
emailstring (email)YesContact email address.
namestringNoContact name.
metadataobjectNoArbitrary key-value pairs for segmentation and template variables.

Response (201 Created):

{
  "id": "ct_abc123",
  "email": "alice@example.com",
  "name": "Alice Kamau",
  "metadata": {"city": "Nairobi", "plan": "pro"},
  "created_at": "2026-04-10T09:00:00Z"
}

cURL

curl -X POST https://govconnect.ke/v1/contacts/cl_abc123/contacts \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "name": "Alice Kamau",
    "metadata": {"city": "Nairobi"}
  }'

Python

import requests
 
response = requests.post(
    "https://govconnect.ke/v1/contacts/cl_abc123/contacts",
    headers={"Authorization": "Bearer tfm_k_YOUR_API_KEY"},
    json={
        "email": "alice@example.com",
        "name": "Alice Kamau",
        "metadata": {"city": "Nairobi"}
    }
)
 
print(response.json())

Node.js

const response = await fetch(
  "https://govconnect.ke/v1/contacts/cl_abc123/contacts",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer tfm_k_YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      email: "alice@example.com",
      name: "Alice Kamau",
      metadata: { city: "Nairobi" },
    }),
  }
);
 
const data = await response.json();
console.log(data);

Remove a contact from a list

DELETE /v1/contacts/{list_id}/contacts/{contact_id}

Removes a single contact from a list.

curl -X DELETE https://govconnect.ke/v1/contacts/cl_abc123/contacts/ct_abc123 \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY"

Response (204 No Content):

No response body.


Upload contacts via CSV

POST /v1/contacts/{list_id}/upload

Imports contacts from a CSV file. The endpoint auto-detects the email column and deduplicates contacts within the list.

Request: multipart/form-data with a file field.

The CSV file should have headers in the first row. The endpoint looks for a column named email (case-insensitive). Additional columns are stored as contact metadata.

Example CSV:

email,name,city,plan
alice@example.com,Alice Kamau,Nairobi,pro
bob@example.com,Bob Otieno,Mombasa,free
charlie@example.com,Charlie Wanjiku,Kisumu,starter

cURL

curl -X POST https://govconnect.ke/v1/contacts/cl_abc123/upload \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY" \
  -F "file=@contacts.csv"

Python

import requests
 
with open("contacts.csv", "rb") as f:
    response = requests.post(
        "https://govconnect.ke/v1/contacts/cl_abc123/upload",
        headers={"Authorization": "Bearer tfm_k_YOUR_API_KEY"},
        files={"file": ("contacts.csv", f, "text/csv")}
    )
 
result = response.json()
print(f"Imported: {result['imported']}")
print(f"Skipped: {result['skipped']}")
if result["errors"]:
    print(f"Errors: {result['errors']}")

Node.js

const formData = new FormData();
formData.append("file", new Blob([csvContent], { type: "text/csv" }), "contacts.csv");
 
const response = await fetch(
  "https://govconnect.ke/v1/contacts/cl_abc123/upload",
  {
    method: "POST",
    headers: { "Authorization": "Bearer tfm_k_YOUR_API_KEY" },
    body: formData,
  }
);
 
const result = await response.json();
console.log(`Imported: ${result.imported}`);
console.log(`Skipped: ${result.skipped}`);

Response:

{
  "imported": 245,
  "skipped": 12,
  "errors": [
    "Row 18: invalid email format 'not-an-email'",
    "Row 45: missing email column value"
  ]
}

Duplicate contacts (same email within the list) are skipped, not rejected. The skipped count includes both duplicates and rows with missing email values.


Bulk send to a contact list

POST /v1/contacts/{list_id}/send

Sends an email to all contacts in a list. Supports template variables that are replaced per contact.

Request body:

{
  "contact_list_id": "cl_abc123",
  "sender_address_id": "sa_abc123",
  "subject": "Welcome, {{name}}!",
  "html": "<h1>Hello {{name}}</h1><p>You are based in {{city}}.</p>",
  "text": "Hello {{name}}, you are based in {{city}}.",
  "tags": ["welcome-campaign"]
}
FieldTypeRequiredDescription
contact_list_idUUIDYesThe contact list to send to.
sender_address_idUUIDYesThe sender address to use. Must be active and on a verified domain.
subjectstringYesEmail subject line. Supports template variables.
htmlstringNoHTML body. Supports template variables.
textstringNoPlain-text body. Supports template variables. At least one of html or text is required.
tagsstring[]NoTags for filtering in logs and analytics.

Template variables

Use {{variable}} syntax in the subject, HTML body, and text body. The following variables are available per contact:

VariableSource
{{email}}Contact's email address.
{{name}}Contact's name. Falls back to empty string if not set.
{{metadata_key}}Any key from the contact's metadata object (e.g. {{city}}, {{plan}}).

cURL

curl -X POST https://govconnect.ke/v1/contacts/cl_abc123/send \
  -H "Authorization: Bearer tfm_k_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_list_id": "cl_abc123",
    "sender_address_id": "sa_abc123",
    "subject": "Welcome, {{name}}!",
    "html": "<h1>Hello {{name}}</h1><p>Thanks for joining.</p>",
    "tags": ["welcome-campaign"]
  }'

Python

import requests
 
response = requests.post(
    "https://govconnect.ke/v1/contacts/cl_abc123/send",
    headers={"Authorization": "Bearer tfm_k_YOUR_API_KEY"},
    json={
        "contact_list_id": "cl_abc123",
        "sender_address_id": "sa_abc123",
        "subject": "Welcome, {{name}}!",
        "html": "<h1>Hello {{name}}</h1><p>Thanks for joining.</p>",
        "tags": ["welcome-campaign"]
    }
)
 
result = response.json()
print(f"Queued: {result['queued']}")

Node.js

const response = await fetch(
  "https://govconnect.ke/v1/contacts/cl_abc123/send",
  {
    method: "POST",
    headers: {
      "Authorization": "Bearer tfm_k_YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      contact_list_id: "cl_abc123",
      sender_address_id: "sa_abc123",
      subject: "Welcome, {{name}}!",
      html: "<h1>Hello {{name}}</h1><p>Thanks for joining.</p>",
      tags: ["welcome-campaign"],
    }),
  }
);
 
const result = await response.json();
console.log(`Queued: ${result.queued}`);

Response:

{
  "queued": 1238,
  "skipped": 12,
  "errors": [
    "Contact ct_xyz789: sender address is not active"
  ]
}

Contacts without a valid email address and contacts that have previously bounced or unsubscribed are automatically skipped.


Errors

StatusDescription
400 Bad RequestThe uploaded CSV is empty, has no data rows, or has no detectable email column; or contact_list_id in a bulk-send body does not match the URL; or the contact list is empty.
401 UnauthorizedMissing or invalid authentication.
403 ForbiddenA plan limit was reached (contact lists, contacts per list).
404 Not FoundThe contact list, contact, or sender address does not exist or is not visible to your workspace.
409 ConflictA contact with this email already exists in the list.
422 Unprocessable EntityRequest body validation failed.