mailtanimailtani

Contacts

Manage your contacts with full CRUD operations. Creating a contact with an existing email will upsert (update the existing record).

List Contacts

GET
/contacts

Returns a paginated list of contacts. Supports filtering by search term, list, tag, status, and source.

Query Parameters

search

string

Search by email, first name, or last name.

Optional
list_id

integer

Filter by list ID.

Optional
tag_id

integer

Filter by tag ID.

Optional
status

string

Filter by status: `active`, `bounced`, `unsubscribed`, or `complained`.

Optional
source

string

Filter by source: `manual`, `import`, `api`, or `scraper`.

Optional
page

integer

default: 1

Page number.

Optional

Example

curl "https://mailtani.com/api/v1/contacts?search=jane&status=active" \
  -H "Authorization: Bearer mk_your_api_key_here"

Response 200 OK

{
  "data": [
    {
      "id": 1,
      "email": "[email protected]",
      "first_name": "Jane",
      "last_name": "Doe",
      "website_url": "https://janedoe.com",
      "status": "active",
      "source": "api",
      "lists": [
        { "id": 1, "name": "Newsletter", "color": "#228be6" }
      ],
      "tags": [
        { "id": 1, "name": "VIP", "color": "#fa5252" }
      ],
      "custom_values": { "company": "Acme Corp" },
      "bounced_at": null,
      "unsubscribed_at": null,
      "last_replied_at": null,
      "created_at": "2026-01-10T08:00:00+00:00",
      "updated_at": "2026-01-12T14:30:00+00:00"
    }
  ],
  "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta": { "current_page": 1, "per_page": 25, "total": 150 }
}

Create a Contact

POST
/contacts

Creates a new contact. If a contact with the same email already exists, the existing contact is updated instead (upsert) and a 200 status is returned.

Body

email

string

Contact email address.

Required
first_name

string

First name.

Optional
last_name

string

Last name.

Optional
website_url

string

Website URL.

Optional
list_ids

array

Array of list IDs to assign the contact to.

Optional
tag_ids

array

Array of tag IDs to assign to the contact.

Optional
custom_values

object

Key-value pairs of custom field values.

Optional

Example

curl -X POST https://mailtani.com/api/v1/contacts \
  -H "Authorization: Bearer mk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "first_name": "Jane",
    "last_name": "Doe",
    "website_url": "https://janedoe.com",
    "list_ids": [1, 2],
    "tag_ids": [3],
    "custom_values": { "company": "Acme Corp" }
  }'

Response 201 Created

{
  "data": {
    "id": 1,
    "email": "[email protected]",
    "first_name": "Jane",
    "last_name": "Doe",
    "website_url": "https://janedoe.com",
    "status": "active",
    "source": "api",
    "lists": [
      { "id": 1, "name": "Newsletter", "color": "#228be6" }
    ],
    "tags": [
      { "id": 3, "name": "VIP", "color": "#fa5252" }
    ],
    "custom_values": { "company": "Acme Corp" },
    "bounced_at": null,
    "unsubscribed_at": null,
    "last_replied_at": null,
    "created_at": "2026-01-10T08:00:00+00:00",
    "updated_at": "2026-01-10T08:00:00+00:00"
  }
}

Get a Contact

GET
/contacts/{id}

Example

curl https://mailtani.com/api/v1/contacts/1 \
  -H "Authorization: Bearer mk_your_api_key_here"

Response 200 OK — returns the same contact object as Create.

Update a Contact

PUT
/contacts/{id}

Partially updates a contact. Only the provided fields will be changed.

Body

email

string

Updated email address.

Optional
first_name

string

Updated first name.

Optional
last_name

string

Updated last name.

Optional
website_url

string

Updated website URL.

Optional
list_ids

array

Replace all list assignments with these list IDs.

Optional
tag_ids

array

Replace all tag assignments with these tag IDs.

Optional
custom_values

object

Upsert custom field values. Existing keys not included are left unchanged.

Optional

Example

curl -X PUT https://mailtani.com/api/v1/contacts/1 \
  -H "Authorization: Bearer mk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Janet",
    "tag_ids": [3, 5]
  }'

Response 200 OK — returns the updated contact object.

Delete a Contact

DELETE
/contacts/{id}

Permanently deletes a contact and all associated data.

Example

curl -X DELETE https://mailtani.com/api/v1/contacts/1 \
  -H "Authorization: Bearer mk_your_api_key_here"

Response 204 No Content

Forget a Contact

POST
/contacts/forget

Use this when your product deletes a user's account. In one call it:

  • stops every active, pending and paused campaign or flow enrollment for the contact
  • deletes any one-off emails to the contact that haven't been sent yet (drafts and scheduled)
  • archives the contact, so no flow, sequence, broadcast or one-off email can reach them again

The contact is archived, not deleted. The archived row acts as a suppression entry: if your product later syncs the same email with POST /contacts, the contact stays archived and unmailable. Use DELETE /contacts/{id} if you need the record gone entirely.

Contacts that already bounced or complained keep that status. Calling this more than once is safe.

Sending a deletion confirmation

The goodbye email isn't part of the request. You build it in Mailtani as a farewell flow, then ask for it with send_farewell: true.

A farewell flow is a flow with the Event Received trigger and the event name set to exactly contact.forgotten. After archiving the contact, Mailtani fires that event. Farewell flows are the only flows allowed to enroll and email an archived or unsubscribed contact. They never email a contact who bounced or complained.

A flow with no event name set matches every event, but it is not a farewell flow and never picks up a forgotten contact. Farewell flows are transactional, so set ignore_frequency_cap on them and leave open tracking off.

The event carries reason as a property. Contact fields such as {{firstName}} resolve as usual in the email.

Body

email

string

Email address of the contact to forget. Case-insensitive.

Required
reason

string

Why the contact is being forgotten, e.g. "account_deleted". Logged, and passed to the farewell flow as an event property.

Optional
send_farewell

boolean

Fire contact.forgotten after archiving so a farewell flow can send the deletion confirmation. Defaults to false.

Optional

Example

curl -X POST https://mailtani.com/api/v1/contacts/forget \
  -H "Authorization: Bearer mk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "reason": "account_deleted",
    "send_farewell": true
  }'

Response 200 OK

{
  "data": {
    "found": true,
    "contact_id": 42,
    "status": "archived",
    "stopped_enrollments": 2,
    "cancelled_emails": 0,
    "farewell_enrolled": true
  }
}

found is false when the email wasn't a contact yet. Without send_farewell, nothing is created. With it, the contact is created already archived so the farewell flow can reach them.

farewell_enrolled is false when no active farewell flow exists, or when the contact bounced or complained.