Account
Account Info
Bulk Operations
Bulk Upsert Update Tags
Templates
List Templates
Usage & Quota
Account Usage
Campaign Actions
Trigger Send

REST API v1

The MailForge API lets you integrate email marketing into any application. Push contacts from your CRM, trigger campaigns, and receive real-time webhook events. All endpoints return JSON.

All API endpoints are prefixed with /api/v1/. The API is stateless — include your API key on every request.

Authentication

All requests require an API key, generated from Settings → API Keys. Pass it as a Bearer token or via the X-API-Key header.

cURL
# Option 1: Authorization header (recommended)
curl -H "Authorization: Bearer mf_your_api_key_here" \
     https://yourapp.com/api/v1/account

# Option 2: X-API-Key header
curl -H "X-API-Key: mf_your_api_key_here" \
     https://yourapp.com/api/v1/contacts
Never expose your API key in client-side JavaScript. Use a backend proxy for browser-based apps.

Base URL

Replace https://yourapp.com with your MailForge instance URL. All endpoints are relative to:

Base URL
https://yourapp.com/api/v1/

Error Handling

All responses use the same envelope. Check success and the HTTP status code.

Error Response
{
  "success": false,
  "error": "email is required"
}
StatusMeaning
200Success
201Resource created
400Bad request — missing or invalid parameters
401Unauthorized — invalid or missing API key
403Forbidden — organization inactive
404Resource not found
500Server error
GET /api/v1/contacts

Returns paginated contacts for your organization.

ParameterTypeDescription
pageoptintegerPage number, default 1
per_pageoptintegerResults per page, max 100, default 25
statusoptstringFilter: active, unsubscribed, bounced, complained
list_idoptintegerFilter by contact list ID
qoptstringSearch by email address
cURL
curl -H "Authorization: Bearer mf_key" \
     "https://yourapp.com/api/v1/contacts?page=1&per_page=25&status=active"
200 Response
{
  "success": true,
  "data": [{ "id": 1, "email": "alice@example.com", "first_name": "Alice", "status": "active", ... }],
  "meta": { "page": 1, "per_page": 25, "total": 150, "pages": 6 }
}
POST /api/v1/contacts

Create a new contact, or update an existing one if the email already exists (upsert).

FieldTypeDescription
emailreqstringContact email address
first_nameoptstringFirst name
last_nameoptstringLast name
phoneoptstringPhone number
companyoptstringCompany name
cityoptstringCity
countryoptstringCountry
statusoptstringactive (default), unsubscribed
sourceoptstringOrigin: api, crm, import (default: api)
list_idsoptarrayList IDs to add this contact to
cURL
curl -X POST https://yourapp.com/api/v1/contacts \
  -H "Authorization: Bearer mf_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@acme.com",
    "first_name": "John",
    "last_name": "Doe",
    "company": "ACME Corp",
    "source": "crm",
    "list_ids": [1, 3]
  }'
POST /api/v1/contacts/subscribe

Quick subscribe endpoint, ideal for web forms, popups, and landing pages. Handles new contacts and re-subscribes existing ones.

JavaScript — Web Form
// Call from your website's email signup form
async function subscribe(email, name) {
  const res = await fetch('/api/v1/contacts/subscribe', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer mf_your_api_key',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email, first_name: name,
      list_id: 1,       // optional: add to specific list
      source: 'website-footer'
    })
  });
  const data = await res.json();
  // { success: true, data: { subscribed: true, email: "..." } }
}
GET/api/v1/contacts/{id}

Get a single contact by ID.

cURL
curl -H "Authorization: Bearer mf_key" https://yourapp.com/api/v1/contacts/42
PUT PATCH/api/v1/contacts/{id}

Update contact fields. Send only the fields you want to change.

cURL
curl -X PUT https://yourapp.com/api/v1/contacts/42 \
  -H "Authorization: Bearer mf_key" \
  -H "Content-Type: application/json" \
  -d '{"company": "New Corp", "status": "active"}'
DELETE/api/v1/contacts/{id}

Permanently delete a contact and all associated send records from your organization.

cURL
curl -X DELETE -H "Authorization: Bearer mf_key" https://marketing.bithost.in/api/v1/contacts/42
200 Response
{
  "success": true,
  "data": { "deleted": true, "id": 42 }
}
DELETE/api/v1/contacts/{id}

Permanently delete a contact. This cannot be undone.

cURL
curl -X DELETE -H "Authorization: Bearer mf_key" https://yourapp.com/api/v1/contacts/42
POST/api/v1/contacts/unsubscribe

Mark a contact as unsubscribed. Fire a contact.unsubscribed webhook event.

cURL
curl -X POST https://yourapp.com/api/v1/contacts/unsubscribe \
  -H "Authorization: Bearer mf_key" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com"}'
GET/api/v1/lists

List all contact lists for your organization.

200 Response
{
  "success": true,
  "data": [
    { "id": 1, "name": "Newsletter Subscribers", "subscriber_count": 1245 },
    { "id": 2, "name": "Product Updates", "subscriber_count": 820 }
  ]
}
POST/api/v1/lists

Create a new contact list.

cURL
curl -X POST https://yourapp.com/api/v1/lists \
  -H "Authorization: Bearer mf_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "VIP Customers", "description": "High-value segment"}'
GET/api/v1/campaigns

List campaigns with status and basic stats.

ParameterTypeDescription
statusoptstringFilter: draft, scheduled, sending, sent, paused, cancelled
GET/api/v1/campaigns/{id}/stats

Full analytics for a campaign.

200 Response
{
  "success": true,
  "data": {
    "campaign_id": 5,
    "name": "April Newsletter",
    "stats": {
      "total_recipients": 1200, "sent": 1198, "delivered": 1185,
      "delivery_rate": 98.9, "unique_opens": 423, "open_rate": 35.7,
      "unique_clicks": 89, "click_rate": 7.5, "ctor": 21.0,
      "bounced": 13, "bounce_rate": 1.1,
      "unsubscribed": 4, "unsubscribe_rate": 0.3,
      "spam_complaints": 0, "complaint_rate": 0.0
    }
  }
}
GET/api/v1/account

Organization account info including current plan and email usage.

Webhook Events

Configure webhooks in Settings → Integrations. MailForge will POST to your endpoint when these events occur.

contact.created
A new contact was added via API or import
contact.updated
A contact's fields were updated
contact.subscribed
A contact subscribed via /subscribe endpoint
contact.unsubscribed
A contact unsubscribed from a campaign
email.opened
An email was opened (first/unique open only)
email.clicked
A tracked link was clicked (unique only)
email.bounced
An email hard bounced
email.complained
A spam complaint was received

Webhook Payload

POST your-endpoint.com/webhooks
{
  "event": "contact.subscribed",
  "timestamp": "1712345678",
  "data": {
    "id": 123,
    "email": "user@example.com",
    "first_name": "Jane",
    "company": "Acme Corp",
    "status": "active",
    "source": "website-footer",
    "subscribed_at": "2025-04-06T10:30:00"
  }
}

Signature Verification

Each webhook includes an X-Mailforge-Signature header (HMAC-SHA256 of the raw body using your webhook secret). Verify it to ensure the request is authentic.

Node.js — Verify Signature
const crypto = require('crypto');

function verifyWebhook(req, webhookSecret) {
  const signature = req.headers['x-mailforge-signature'];
  const timestamp  = req.headers['x-mailforge-timestamp'];
  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(`${timestamp}.${JSON.stringify(req.body)}`)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature), Buffer.from(expected)
  );
}
POST/api/v1/contacts/bulk

Bulk upsert up to 1,000 contacts in a single call. Existing contacts are updated; new ones are created. Suppressed emails are silently skipped.

FieldTypeDescription
contactsreqarrayArray of contact objects (max 1,000)
list_idoptintegerAdd all contacts to this list
cURL
curl -X POST https://yourapp.com/api/v1/contacts/bulk \
  -H "Authorization: Bearer mf_key" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": 1,
    "contacts": [
      {"email": "alice@company.com", "first_name": "Alice", "company": "Acme"},
      {"email": "bob@company.com", "first_name": "Bob", "source": "crm"}
    ]
  }'
200 Response
{ "success": true, "data": { "created": 1, "updated": 1, "skipped": 0, "total": 2 } }
PUT/api/v1/contacts/{id}/tags

Replace the full tag set for a contact.

cURL
curl -X PUT https://yourapp.com/api/v1/contacts/42/tags \
  -H "Authorization: Bearer mf_key" \
  -d '{"tags": ["vip", "enterprise", "demo-attended"]}'
GET/api/v1/templates

List all active email templates in your organization.

GET/api/v1/suppressions

List all suppressed email addresses (unsubscribes, bounces, complaints, and manually suppressed).

200 Response
[{ "email": "user@old.com", "reason": "unsubscribed", "added_at": "2025-03-01T..." }]
POST/api/v1/suppressions

Add an email address to the global suppression list. Suppressed addresses are never sent to, even if re-added to a contact list.

cURL
curl -X POST https://yourapp.com/api/v1/suppressions \
  -d '{"email": "optout@example.com", "reason": "manual"}'
DELETE/api/v1/suppressions/{email}

Remove an email from the suppression list (re-enables sending to this address).

Account Usage & Quota

GET/api/v1/account/usage

Real-time usage and quota information for your organization.

200 Response
{
  "plan": "growth", "subscription_status": "active",
  "usage": {
    "emails_sent": 4820, "emails_limit": 25000, "emails_pct": 19.3,
    "ai_used": 312, "ai_limit": 8000, "ai_remaining": 7688
  }
}

Inbound Email Parse

Each inbound handler has a unique address (token@inbound.marketing.bithost.in). Configure your DNS MX record or email routing rule to POST parsed emails to the receive endpoint. MailForge automatically creates contacts, fires webhooks, and logs every received email.

Works with any SMTP relay: Postfix, Haraka, PowerMTA, Cloudflare Email Routing, AWS SES inbound, Mailgun Routes.
GET/api/v1/inbound

List all inbound email handlers for your organization.

200 Response
[{
  "id": 1, "name": "Lead Capture", "is_active": true,
  "inbound_address": "abc123xyz@inbound.marketing.bithost.in",
  "emails_received": 142, "forward_url": "https://yourcrm.com/hooks/email",
  "auto_create_contact": true, "auto_tag": "inbound,lead"
}]

Inbound Webhook Payload

When an email arrives at your inbound address and you have a forward URL configured, MailForge POSTs this signed JSON to your endpoint.

Inbound Payload
{
  "event":            "email.received",
  "timestamp":        "2025-04-15T10:30:00Z",
  "inbound_address":  "abc123@inbound.marketing.bithost.in",
  "from_email":       "customer@company.com",
  "from_name":        "Jane Doe",
  "subject":          "Interested in your Growth plan",
  "body_text":        "Hi, I'd like more information...",
  "attachments":      0,
  "contact": {
    "id":      1842,
    "email":   "customer@company.com",
    "created": true
  }
}

Verifying Inbound Signatures

Node.js
const crypto = require('crypto');

function verifyInbound(req, forwardSecret) {
  const sig = req.headers['x-mailforge-signature'];
  const expected = crypto
    .createHmac('sha256', forwardSecret)
    .update(req.rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(sig, 'hex'),
    Buffer.from(expected, 'hex')
  );
}
POST/api/v1/campaigns/{id}/send

Trigger a draft or scheduled campaign to send immediately. Plan limits are checked before sending.

Returns 402 if your monthly email quota is exhausted. Check /api/v1/account/usage before triggering.
cURL
curl -X POST https://yourapp.com/api/v1/campaigns/5/send \
  -H "Authorization: Bearer mf_key"
200 Response
{ "success": true, "data": { "campaign_id": 5, "status": "sending" } }
GET/api/v1/inbound

List all inbound email handlers for your organization.

200 Response
{
  "success": true,
  "data": [{
    "id": 1,
    "name": "Sales Inbox",
    "inbound_address": "abc123@inbound.marketing.bithost.in",
    "is_active": true,
    "emails_received": 42,
    "auto_create_contact": true,
    "auto_tag": "inbound,lead"
  }]
}
POST/api/v1/inbound

Create a new inbound email handler. Returns the unique inbound email address and signing secret.

FieldTypeDescription
nameoptstringLabel for this handler (default: "Inbound Handler")
forward_urloptstringHTTPS URL to POST parsed email payload to
auto_create_contactoptboolAuto-create contact from sender (default: true)
auto_tagoptstringComma-separated tags to apply to created contact
list_idoptintegerAdd created contact to this list
cURL
curl -X POST https://yourapp.com/api/v1/inbound \
  -H "Authorization: Bearer mf_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sales Inbox",
    "forward_url": "https://yourcrm.com/webhooks/email",
    "auto_create_contact": true,
    "auto_tag": "inbound,hot-lead",
    "list_id": 1
  }'
201 Response
{
  "success": true,
  "data": {
    "id": 1,
    "inbound_address": "abc123@inbound.marketing.bithost.in",
    "secret": "wh_sec_xxxxxxxx"
  }
}

Inbound Receive Endpoint

This is the endpoint your MX handler or SMTP relay POSTs parsed emails to. It's a public endpoint (no API key needed) — secured by the inbound address token in the URL.

POST/inbound/receive/{token}

Receive a parsed inbound email. Configure your mail server to POST here.

FieldTypeDescription
from_emailreqstringSender email address
from_nameoptstringSender display name
subjectoptstringEmail subject line
body_textoptstringPlain text body
body_htmloptstringHTML body
headersoptobjectRaw email headers
attachmentsoptarrayAttachment metadata
cURL — from your MX handler
curl -X POST https://yourapp.com/inbound/receive/abc123token \
  -H "Content-Type: application/json" \
  -H "X-Mailforge-Signature: sha256=..." \
  -d '{
    "from_email": "lead@acmecorp.com",
    "from_name": "Priya Sharma",
    "subject": "Interested in your Pro plan",
    "body_text": "Hi, I would like to learn more...",
    "attachments": []
  }'

Inbound Webhook Payload

After processing, MailForge POSTs this signed payload to your forward_url.

Webhook sent to your forward_url
{
  "event":          "email.received",
  "timestamp":      "2025-04-15T09:22:14Z",
  "inbound_address":"abc123@inbound.marketing.bithost.in",
  "from_email":     "lead@acmecorp.com",
  "from_name":      "Priya Sharma",
  "subject":        "Interested in your Pro plan",
  "body_text":      "Hi, I would like to learn more...",
  "attachments":    0,
  "contact": {
    "id":      1842,
    "email":   "lead@acmecorp.com",
    "created": true,
    "tags":    "inbound,hot-lead",
    "list_added": "Newsletter Subscribers"
  }
}
// Headers included on every inbound forward:
// X-Mailforge-Signature: hmac-sha256-of-body
// X-Mailforge-Event:     email.received
// X-Mailforge-Timestamp: unix-timestamp
Verify the X-Mailforge-Signature header on every inbound forward using HMAC-SHA256 with the secret returned when you created the handler.
GET/api/v1/webhooks

List all outbound webhook subscriptions for your organization.

POST/api/v1/webhooks

Subscribe to outbound events. Returns the signing secret to verify incoming payloads.

FieldTypeDescription
urlreqstringHTTPS endpoint to deliver events to
eventsreqarrayList of event names to subscribe to
nameoptstringDisplay name for this webhook
cURL
curl -X POST https://yourapp.com/api/v1/webhooks \
  -H "Authorization: Bearer mf_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourcrm.com/webhooks/mailforge",
    "name": "CRM Sync",
    "events": [
      "contact.subscribed",
      "contact.unsubscribed",
      "email.opened",
      "email.clicked"
    ]
  }'
GET/api/v1/campaigns/{id}

Get full details of a single campaign.

cURL
curl -H "Authorization: Bearer mf_key" https://yourapp.com/api/v1/campaigns/5
GET/api/v1/campaigns/{id}/sends

Paginated list of individual send records for a campaign. Filterable by status.

ParameterTypeDescription
statusoptstringsent | bounced | failed | pending
POST/api/v1/campaigns/{id}/send

Trigger an immediate send for a draft, paused, or scheduled campaign. Performs quota check before queuing.

Returns 402 if insufficient monthly email quota. Upgrade your plan before retrying.
cURL
curl -X POST -H "Authorization: Bearer mf_key" https://yourapp.com/api/v1/campaigns/5/send
200 Response
{
  "success": true,
  "data": {
    "queued": true,
    "campaign_id": 5,
    "message": "Campaign queued for sending."
  }
}
GET/api/v1/lists/{id}

Get a list with its paginated active contacts.

cURL
curl -H "Authorization: Bearer mf_key" "https://yourapp.com/api/v1/lists/1?page=1&per_page=25"
POST/api/v1/lists/{id}/contacts

Add an existing contact (by email) to a list.

cURL
curl -X POST https://yourapp.com/api/v1/lists/1/contacts \
  -H "Authorization: Bearer mf_key" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com"}'
GET/api/v1/account/usage

Current month usage plus last 12 months send history for charting.

200 Response
{
  "success": true,
  "data": {
    "current_month": { "sent": 18, "limit": 25000, "pct": 0.1 },
    "history": [
      { "year": 2025, "month": 3, "sent": 12450 },
      { "year": 2025, "month": 4, "sent": 18900 }
    ]
  }
}

JavaScript / Node.js

Pure fetch — works in browsers, Node.js 18+, Deno, and Bun. No dependencies required.

JavaScript / Node.js
class MailForge {
  constructor(apiKey, baseUrl = 'https://yourapp.com') {
    this.baseUrl = baseUrl;
    this.headers = {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    };
  }
  async subscribe(data) {
    const r = await fetch(`${this.baseUrl}/api/v1/contacts/subscribe`, {
      method: 'POST', headers: this.headers, body: JSON.stringify(data)
    });
    return r.json();
  }
  async getContacts(params = {}) {
    const q = new URLSearchParams(params);
    const r = await fetch(`${this.baseUrl}/api/v1/contacts?${q}`, { headers: this.headers });
    return r.json();
  }
  async getCampaignStats(campaignId) {
    const r = await fetch(`${this.baseUrl}/api/v1/campaigns/${campaignId}/stats`, { headers: this.headers });
    return r.json();
  }
}

// Usage
const mf = new MailForge('mf_your_api_key');
await mf.subscribe({ email: 'user@example.com', list_id: 1 });

PHP SDK

Use PHP's built-in cURL or Guzzle to integrate with the MailForge REST API. No dedicated SDK required — the API is plain JSON over HTTPS.

PHP — Subscribe a contact
<?php
// Install Guzzle: composer require guzzlehttp/guzzle
// Or use plain cURL (shown below)

$apiKey = 'mf_your_api_key_here';
$baseUrl = 'https://marketing.bithost.in/api/v1';

// ── Subscribe a contact ──────────────────────────
$ch = curl_init($baseUrl . '/contacts/subscribe');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST           => true,
  CURLOPT_HTTPHEADER     => [
    'Authorization: Bearer ' . $apiKey,
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'email'      => 'user@example.com',
    'first_name' => 'Priya',
    'last_name'  => 'Sharma',
    'list_id'    => 1,
    'tags'       => 'website,lead',
  ]),
]);
$response = curl_exec($ch);
$data     = json_decode($response, true);
curl_close($ch);

if ($data['success']) {
  echo 'Subscribed: ' . $data['data']['email'];
}
PHP — List contacts with pagination
$page     = 1;
$perPage  = 25;
$url      = $baseUrl . "/contacts?page={$page}&per_page={$perPage}&status=active";

$ch = curl_init($url);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER     => [
    'Authorization: Bearer ' . $apiKey,
  ],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);

foreach ($data['data'] as $contact) {
  echo $contact['email'] . PHP_EOL;
}
// Pagination info in $data['meta']
// total, pages, has_next, has_prev
PHP — Trigger campaign send
$campaignId = 42;
$ch = curl_init($baseUrl . "/campaigns/{$campaignId}/send");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST           => true,
  CURLOPT_HTTPHEADER     => [
    'Authorization: Bearer ' . $apiKey,
  ],
  CURLOPT_POSTFIELDS => '',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $data['data']['message']; // "Campaign queued for sending."
PHP — Verify inbound webhook signature
// In your webhook endpoint (receives inbound email events)
$secret  = 'your_forward_secret_from_handler';
$payload = file_get_contents('php://input');
$sig     = $_SERVER['HTTP_X_MAILFORGE_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expected, $sig)) {
  http_response_code(401);
  die('Invalid signature');
}

$event = json_decode($payload, true);
$email = $event['from_email'];
$subj  = $event['subject'];

// Process the inbound email event...
echo json_encode(['status' => 'ok']);
PHP — Bulk import contacts
$contacts = [
  ['email' => 'alice@example.com', 'first_name' => 'Alice', 'company' => 'ACME'],
  ['email' => 'bob@example.com',   'first_name' => 'Bob'],
  // ... up to 500 per request
];

$ch = curl_init($baseUrl . '/contacts/bulk');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST           => true,
  CURLOPT_HTTPHEADER     => [
    'Authorization: Bearer ' . $apiKey,
    'Content-Type: application/json',
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'contacts' => $contacts,
    'list_id'  => 1, // optional
  ]),
]);
$result = json_decode(curl_exec($ch), true)['data'];
curl_close($ch);

echo "Created: {$result['created']}, Updated: {$result['updated']}, Failed: {$result['failed']}";

Python SDK

Python — requests
import requests

class MailForge:
    def __init__(self, api_key, base_url="https://yourapp.com"):
        self.base_url = base_url
        self.headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}

    def subscribe(self, email, **kwargs):
        return requests.post(f"{self.base_url}/api/v1/contacts/subscribe",
            json={"email": email, **kwargs}, headers=self.headers).json()

    def get_contacts(self, **params):
        return requests.get(f"{self.base_url}/api/v1/contacts",
            params=params, headers=self.headers).json()

    def campaign_stats(self, campaign_id):
        return requests.get(f"{self.base_url}/api/v1/campaigns/{campaign_id}/stats",
            headers=self.headers).json()

# Usage
mf = MailForge("mf_your_api_key")
mf.subscribe("user@example.com", first_name="Jane", list_id=1)