Skip to main content

Zammad Integration

Overview

Verified contact form submissions are forwarded to the Zammad helpdesk as tickets via the Zammad REST API. A ticket is created only after the Reoon email verification passes — failed verifications never reach Zammad.

Notification scope: Zammad is used for ticket intake only. Telegram and email notifications are sent by the contact form snippet on WordPress (see Telegram Notifications and Email Notifications); Zammad does not trigger those. Agents reply to customers inside Zammad via the email channel.

Architecture

Zammad Setup

  • Host: Zammad 7.x runs as a Docker Compose stack on GSM16 (/opt/docker-data/apps/zammad/), exposed via Cloudflare tunnel at https://zammad.id86.net — see Zammad Stack
  • Group: opshell.dev — target group for contact-form tickets, assigned outbound email support@opshell.dev
  • Authentication: HTTP token auth (Authorization: Token token=...) for an agent with ticket.agent permission
  • Token, URL, and group are stored as WordPress options (never hardcoded in the snippet)

Group email requirement: for agent email replies to reach customers, the ticket group must have an outbound email address assigned (Zammad Admin → Groups → opshell.dev → email address support@opshell.dev). Replies sent as email articles (the Reply composer) go out as support@opshell.dev; internal notes are never emailed.

One-Time Setup (Rails console on GSM16)

The following were executed once to prepare Zammad (via sudo docker exec zammad-zammad-railsserver-1 /opt/zammad/bin/rails r '...'):

  1. Create the ticket group:
    u = User.find_by(login: "donnyaw@gmail.com")
    g = Group.new(name: "opshell.dev", active: true)
    g.created_by_id = u.id
    g.updated_by_id = u.id
    g.save
  2. Grant the agent access to the group:
    u = User.find_by(login: "donnyaw@gmail.com")
    u.groups << Group.find_by(name: "opshell.dev")
    u.save
  3. Create the API token (persistent, ticket.agent permission only):
    t = Token.create!(
    action: "api",
    persistent: true,
    user_id: User.find_by(login: "donnyaw@gmail.com").id,
    name: "opshell-contact-form",
    preferences: { permission: { "ticket.agent" => true } }
    )
    puts t.token # raw token to store in WordPress
  4. Assign the outbound email address to the group (enables agent email replies):
    g = Group.find_by(name: "opshell.dev")
    g.email_address_id = EmailAddress.find_by(email: "support@opshell.dev").id
    g.updated_by_id = User.find_by(login: "donnyaw@gmail.com").id
    g.save

Configuration

SettingOption NameDefault
API base URLopshell_zammad_urlhttps://zammad.id86.net
API tokenopshell_zammad_api_token
Ticket groupopshell_zammad_groupopshell.dev

Set via WP-CLI:

wp option add opshell_zammad_url 'https://zammad.id86.net'
wp option add opshell_zammad_api_token '<TOKEN>'
wp option add opshell_zammad_group 'opshell.dev'

Ticket Creation

opshell_send_contact_zammad() posts to POST {url}/api/v1/tickets:

wp_remote_post(
untrailingslashit( $url ) . "/api/v1/tickets",
array(
"headers" => array(
"Authorization" => "Token token=" . $api_token,
"Content-Type" => "application/json",
),
"body" => wp_json_encode( array(
"title" => "New contact: " . $subject,
"group" => $group,
"customer_id" => "guess:" . $email,
"article" => array(
"subject" => "Contact form: " . $subject,
"body" => $body, // name, email, ip, message, Reoon results
"type" => "web",
"internal" => false,
),
) ),
)
);

Key behaviors:

  • customer_id: "guess:{email}" — looks up the user by email or auto-creates a customer (no extra API call)
  • The article body includes the full submission: name, email, IP, message, and Reoon verification results
  • Returns true on HTTP 200/201; ticket creation failure is non-blocking (does not affect the visitor's success message)
  • Success requires opshell_zammad_url and opshell_zammad_api_token to be set

Snippet Code (WPCodeBox2, ID 1)

The function lives in the "Opshell Contact Form Shortcode" snippet on GC-SG-M8:

function opshell_send_contact_zammad( $name, $email, $subject, $message, $verification ) {
$url = get_option( "opshell_zammad_url", "" );
$api_token = get_option( "opshell_zammad_api_token", "" );
$group = get_option( "opshell_zammad_group", "opshell.dev" );

if ( ! $url || ! $api_token ) {
return false;
}

// ... build article body (name, email, ip, message, Reoon results) ...

$response = wp_remote_post(
untrailingslashit( $url ) . "/api/v1/tickets",
array(
"timeout" => 25,
"headers" => array(
"Authorization" => "Token token=" . $api_token,
"Content-Type" => "application/json",
),
"body" => wp_json_encode( array(
"title" => "New contact: " . $subject,
"group" => $group,
"customer_id" => "guess:" . $email,
"article" => array(
"subject" => "Contact form: " . $subject,
"body" => $body,
"type" => "web",
"internal" => false,
),
) ),
)
);

if ( is_wp_error( $response ) ) {
return false;
}

$code = wp_remote_retrieve_response_code( $response );
return 201 === $code || 200 === $code;
}

It is invoked in opshell_render_contact_form() only after the Reoon is_safe_to_send gate passes and the CSV row is written:

$csv_file = opshell_save_contact_submission_csv( $name, $email, $subject, $message );
if ( ! $csv_file ) {
$error = "Could not save your message. Please try again later.";
} else {
opshell_send_contact_zammad( $name, $email, $subject, $message, $verification );
opshell_send_contact_telegram( $name, $email, $subject, $message, $verification );
opshell_send_contact_notification_email( $name, $email, $subject, $message, $verification );
$sent = true;
}

End-to-End Workflow

Agent Replies (Outbound Email)

  • To reply to a customer, agents must use the email reply composer (creates an email-type article) — internal notes are never emailed.
  • Replies are sent as support@opshell.dev (the group's assigned email address) via Zammad's SMTP channel (mail.opshell.dev:587).
  • The customer's email comes from the ticket's customer record (auto-created from the form email via guess:).

Operations & Troubleshooting

Verify a ticket exists

curl -H "Authorization: Token token=<TOKEN>" \
"https://zammad.id86.net/api/v1/tickets/search?query=<email-or-subject>"

Verify outbound SMTP from Zammad

sudo docker exec zammad-zammad-railsserver-1 /opt/zammad/bin/rails r '
Mail.new(to: "you@example.com", from: "support@opshell.dev",
subject: "test", body: "test").deliver
'

Common issues

SymptomCause / Fix
No ticket createdReoon verification failed (by design — only verified emails reach Zammad); or opshell_zammad_url/token not set
API error "No lookup value for group"Group name mismatch, or token's agent lacks access to the group
Agent reply never emailedReply was a note (not an email article); or the group has no outbound email address assigned
SMTP auth failedOutbound password in Zammad channel settings differs from the mail server's

Endpoint Performance

The default opshell_zammad_url uses the public Cloudflare-tunneled URL (~166ms round-trip, encrypted). For lower latency, the direct internal endpoint on GSM16 (http://195.85.19.185:8080, ~33ms) can be used via the option — but it sends the token over plain HTTP and should only be used on a trusted network.

Security

  • The API token is a secret — stored only as a WordPress option, never in the repo or docs
  • Ticket creation runs only for Reoon-verified submissions
  • The token has ticket.agent permission only (no admin access)