# Zammad → Actions — Setup tutorial

This guide walks through everything needed to connect **Zammad** (e.g. `tickets.asystir.com`) to **Actions UI** so support tickets become Action records when the customer email exists in CRM Contacts.

**Backend spec:** [`../../crmapi/innerdoc/ZAMMAD_ACTIONS_SPEC.md`](../../crmapi/innerdoc/ZAMMAD_ACTIONS_SPEC.md)

---

## What you get

| Feature | Description |
|---------|-------------|
| **Real-time webhook** | New/updated Zammad tickets → Action (if contact exists) |
| **30-day backfill** | “Check on Zammad” in Settings catches missed tickets |
| **Dedup** | Same ticket never creates duplicate Actions (`externalref` = `zammad-ticket-{id}`) |
| **Contact-only rule** | Tickets from unknown emails are skipped (same as Email/WhatsApp loggers) |

There is **no separate Zammad logger UI** — tickets go straight into Actions.

---

## Prerequisites

1. **Zammad** instance reachable over HTTPS (e.g. `https://tickets.asystir.com`)
2. **CRM API** (`crmapi`) deployed and reachable from Zammad (HTTPS recommended)
3. **Database table** `ZammadAccountSettings` created (see below)
4. **Actions UI** with a valid API key for your CRM account
5. **CRM Contacts** with emails matching Zammad customers you want to track

---

## Step 1 — Create the database table

Run once per CRM database (dev/staging/production):

```bash
mysql -u USER -p DATABASE_NAME < /path/to/crmapi/innerdoc/zammad_actions_schema.sql
```

Or execute the SQL manually:

```sql
-- See crmapi/innerdoc/zammad_actions_schema.sql
CREATE TABLE IF NOT EXISTS ZammadAccountSettings ( ... );
```

Verify:

```sql
SHOW TABLES LIKE 'ZammadAccountSettings';
```

---

## Step 2 — Configure Actions UI

1. Open **Actions UI** → **Settings** (gear icon).
2. Scroll to the **Zammad tickets** section.
3. Fill in:

| Field | Example | Notes |
|-------|---------|-------|
| **Zammad domain** | `tickets.asystir.com` | Host only — no `https://` or paths |
| **API token** | *(from Zammad)* | Required for backfill; see Step 4 |
| **Webhook secret** | *(auto-generated on Save)* | Copy this — used in Step 5 |

4. Click **Save Zammad settings**.
5. Copy the **Webhook URL** (button **Copy webhook URL**).

Example webhook URL:

```
https://your-crm-host/WebSite/crmapi/Actions.php?command=zammadwebhook&zammaddomain=tickets.asystir.com
```

The `zammaddomain` query parameter tells the CRM **which account** owns this Zammad instance. Zammad does not send its own hostname in the default payload.

---

## Step 3 — Create a Zammad API token (for backfill)

Used by **Check on Zammad** to pull the last 30 days of tickets.

1. Log into Zammad as an agent/admin.
2. Go to your **Profile** (avatar) → **Token Access** / **API Token**.
3. Create a new token with permission to read tickets (and users if needed).
4. Copy the token → paste into **API token** in Actions UI Settings → **Save** again.

Leave the API token field blank on later saves if you do not want to change it.

---

## Step 4 — Create the Zammad webhook

1. In Zammad: **Manage** → **Webhooks** → **New Webhook**.

### Basic settings

| Field | Value |
|-------|-------|
| **Name** | `CRM Actions` (any label) |
| **Endpoint** | Paste the **Webhook URL** from Actions UI Settings |
| **SSL verification** | **Yes** (use No only for local dev with self-signed certs) |

### Authentication (choose one method)

**Option A — Bearer Token (recommended)**

- Set authentication type to **Bearer Token**.
- Paste the **Webhook secret** from Actions UI Settings as the token value.

**Option B — HMAC SHA1 Signature Token**

- Paste the same **Webhook secret** into **HMAC SHA1 Signature Token**.
- Zammad sends `X-Hub-Signature: sha1=...`; the CRM verifies it automatically.

**Option C — Secret in URL (dev/testing only)**

Append to the endpoint URL:

```
&webhooksecret=YOUR_WEBHOOK_SECRET_HERE
```

Use HTTPS only; prefer Bearer or HMAC in production.

### Custom payload (recommended)

Enable **Custom payload** and use:

```json
{
  "zammaddomain": "#{config.fqdn}",
  "ticket": {
    "id": "#{ticket.id}",
    "number": "#{ticket.number}",
    "title": "#{ticket.title}",
    "state": "#{ticket.state.name}",
    "customer_email": "#{ticket.customer.email}",
    "updated_at": "#{ticket.updated_at}"
  }
}
```

- `#{config.fqdn}` resolves to your Zammad host (e.g. `tickets.asystir.com`) and must match the domain saved in Actions UI.
- `customer_email` is required for contact matching.

Save the webhook.

---

## Step 5 — Create Zammad triggers

Create **two triggers** (or one trigger with an OR condition) so both new and updated tickets fire the webhook.

### Trigger A — Ticket created

1. **Manage** → **Triggers** → **New Trigger**
2. **Name:** `CRM Actions — ticket created`
3. **Condition:** Ticket → Action → **created**
4. **Action:** Notification → **Webhook** → select `CRM Actions`
5. Save

### Trigger B — Ticket updated

1. **New Trigger**
2. **Name:** `CRM Actions — ticket updated`
3. **Condition:** Ticket → Action → **updated**
4. **Action:** Notification → **Webhook** → select `CRM Actions`
5. Save

Avoid overly broad conditions (e.g. every article) unless you want an Action on every ticket change.

---

## Step 6 — Ensure CRM contacts exist

Actions are created **only** when the ticket customer email matches a Contact in the same CRM account:

- Primary email, `email2`, or an entry in the contact `emails` JSON field.

Create or update Contacts in CRM before expecting tickets to sync.

---

## Step 7 — Test the integration

### Test webhook (live)

1. In Zammad, create a test ticket using a customer email that exists in CRM Contacts.
2. In Actions UI, open the list or timeline for that contact — a new Action should appear:
   - **Type:** ticket  
   - **Source:** zammad  
   - **Title:** `Zammad ticket #…: …`

3. Update the ticket title or state in Zammad — the existing Action should update (same `externalref`).

4. Create a ticket with an email **not** in Contacts — webhook succeeds but no Action is created (`skipped_no_contact`).

### Test backfill

1. Actions UI → **Settings** → **Zammad tickets**
2. Click **Check on Zammad**
3. Status shows counts: total scanned, created, updated, skipped, errors

---

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `unknown zammaddomain` | Domain not saved or mismatch | Save domain in Actions UI; ensure URL `zammaddomain=` matches |
| `invalid webhook secret` | Auth not configured | Use Bearer Token or HMAC with the saved webhook secret |
| `zammaddomain mismatch` | Custom payload FQDN differs from Settings | Align Zammad FQDN setting with Actions UI domain |
| Webhook works, no Action | No CRM contact for email | Add Contact with that email |
| Backfill fails | Missing/invalid API token | Create Zammad token; save in Settings |
| SSL errors from Zammad | Self-signed cert on CRM | Dev only: disable SSL verification on webhook |
| Duplicate Actions | Should not happen | Dedup uses `zammad-ticket-{id}`; check `externalref` |

### Manual webhook test (curl)

Replace placeholders:

```bash
curl -X POST \
  'https://YOUR-CRM-HOST/WebSite/crmapi/Actions.php?command=zammadwebhook&zammaddomain=tickets.asystir.com' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_WEBHOOK_SECRET' \
  -d '{
    "zammaddomain": "tickets.asystir.com",
    "ticket": {
      "id": 12345,
      "number": "99001",
      "title": "Test ticket",
      "state": "open",
      "customer_email": "known.contact@example.com",
      "updated_at": "2026-05-29 12:00:00"
    }
  }'
```

Expected success:

```json
{"status":"done","result":"created","actionid":42}
```

If contact missing:

```json
{"status":"done","result":"skipped_no_contact","skipped":true,"reason":"no_contact_match"}
```

---

## How tenant mapping works

```
Zammad (tickets.asystir.com)
        │
        │  POST webhook URL with ?zammaddomain=tickets.asystir.com
        ▼
CRM Actions.php → lookup ZammadAccountSettings by domain → accountid
        │
        │  match customer_email → Contacts
        ▼
Insert or update Action (externalref = zammad-ticket-{id})
```

Zammad’s default payload does **not** include the instance domain. The domain in the webhook URL is the primary identifier. Optional `"zammaddomain": "#{config.fqdn}"` in custom payload adds a second check.

---

## Production checklist

- [ ] Run `zammad_actions_schema.sql` on production DB
- [ ] Set `$ZammadEncryptKey` in production `Config.php`
- [ ] Configure Actions UI (domain, API token, webhook secret)
- [ ] Zammad webhook with HTTPS + Bearer or HMAC
- [ ] Triggers on ticket create and update
- [ ] Confirm Contacts exist for customers you track
- [ ] Run **Check on Zammad** once after go-live
- [ ] Spot-check an Action in timeline after a real ticket

---

## Related files

| File | Purpose |
|------|---------|
| [`crmapi/innerdoc/ZAMMAD_ACTIONS_SPEC.md`](../../crmapi/innerdoc/ZAMMAD_ACTIONS_SPEC.md) | Backend API spec |
| [`crmapi/innerdoc/zammad_actions_schema.sql`](../../crmapi/innerdoc/zammad_actions_schema.sql) | Table DDL |
| [`crmapi/Actions.php`](../../crmapi/Actions.php) | `zammadwebhook`, `synczammad`, settings commands |
| [`crmapi/global.php`](../../crmapi/global.php) | Sync helpers |
| [`Actions.html`](../Actions.html) | Settings UI panel |
| [`Actions.js`](../Actions.js) | Settings + backfill handlers |
