# Freshdesk setup for the Spun helpdesk bridge

Workflow: `spun-freshdesk-bridge.n8n.json` (import into n8n 1.x). Reference facts:
`API-NOTES.md` in this folder (Freshdesk's public API and support documentation, read
10/2026, not yet live-tested). General template notes, node map and the live-test
checklist: `../README.md`.

Every Freshdesk shape below is marked **verified** (stated in Freshdesk's
documentation) or **assumed** (the documentation is ambiguous or silent; the
conservative choice is taken). Treat the assumed items as the first things to check in
the live run.

## 0. What you end up with

| Side | Item |
|---|---|
| Freshdesk | One integration agent and its API key, one or two custom ticket fields ("Spun chat id", optional "Spun line"), and one automation rule on the Ticket Updates tab that calls n8n (two more optional rules for desk-side close and reopen). |
| n8n | The imported workflow, two credentials, the Settings node filled in, the workflow active. |
| Spun | An API key with `send_message` (plus `manage_conversations` if you want desk-side close, step 8) and a webhook subscription with a signing secret. |

Plan requirement (verified): the **Trigger webhook** action in automation rules is
available on the Growth, Pro and Enterprise plans (and the older Blossom, Garden,
Estate and Forest plans), not on Free or Sprout. Without it, WhatsApp messages still
become tickets (that direction uses the API only), but agent notes do not reach
WhatsApp. Creating automation rules and ticket fields needs a Freshdesk **admin**.

How Freshdesk differs from the generic template (resolved here, details below):

- Freshdesk has **no WhatsApp ticket source** in its API. Tickets are created with
  source 7 (Chat) and the tags `spun` and `whatsapp`.
- Phone numbers are not unique on Freshdesk contacts. The workflow looks a contact up
  by phone and by mobile before it creates one, and passes `requester_id` (step 4).
- The Spun chat id lives in a **custom ticket field** you create. Its API name is
  chosen by Freshdesk, so you read it from the API and paste it into Settings (step 5).
- Freshdesk does **not sign** webhook calls. The rule sends a shared secret in a custom
  header, `X-Spun-Secret`, and the workflow compares it (step 6).
- Agents answer WhatsApp tickets with a **public note**. The Reply button sends an
  email, and a WhatsApp customer usually has no email address (step 7).
- The rule body carries ids only; the workflow reads the ticket's conversations through
  the API to find the note text.
- **Closed** (status 5) is final here: a message after a close starts a **new ticket**
  whose description names the previous ticket number. A message on a **Pending** or
  **Resolved** ticket is added as a note and sets it back to Open.
- Inbound messages after the first are public notes written by the integration agent
  on the customer side of the thread (`incoming: true`), starting with the customer's
  name and number.

## 1. Domain

Your Freshdesk URL is `https://<domain>.freshdesk.com`. Put it, without a trailing
slash, into `freshdesk_base`. Use the `freshdesk.com` name even if your support portal
has a custom domain: the custom domain is not the API host. HTTPS only; every API path
is `/api/v2/...` under it. Verified.

## 2. Integration agent

Create (or pick) one full-time agent for the integration, named for example "Spun
WhatsApp". Requirements:

- A role that can view, create and edit tickets, add notes, and view and create
  contacts. The API key carries the agent's role: what the agent cannot do in the
  product, the key cannot do through the API either (verified).
- Ticket scope **Global Access**, so the agent sees every ticket the workflow creates,
  whatever group it lands in (the agent object's `ticket_scope`, 1 = Global, verified;
  that a narrower scope hides tickets from the API is **assumed** from the role rule
  above).
- A dedicated agent keeps the workflow's own writes recognisable in the ticket history
  and keeps them out of the reply path: the workflow never forwards a note written by
  this agent (`desk_integration_user_id`). An agent seat may count against your plan;
  this was not checked.

The agent's id: run `GET {freshdesk_base}/api/v2/agents/me` once with the credential
from step 3 (a temporary HTTP Request node is enough) and copy the top-level `id` into
`desk_integration_user_id`. That endpoint works for any agent, no admin needed
(verified). **Assumed:** this id is the `user_id` Freshdesk puts on the notes this
agent writes (the documentation calls it the agent's user id but shows no note sample
by an agent fetched through `/agents/me`).

## 3. API key and the n8n credential

1. Sign in **as the integration agent**, open Profile settings and copy the API key.
2. In n8n, create a credential of type **Basic Auth**:
   - User: the API key
   - Password: `X` (any dummy value works; the documentation uses `X`)
3. Select this credential on every `FRESHDESK:` node (HTTP Request, Authentication =
   Generic Credential Type, Basic Auth).

Verified: Basic auth with the API key as user name and a dummy password
(`curl -u <apikey>:X`). The key acts as that agent, so keep it in n8n only and rotate it
(Profile settings) when someone who had access to n8n leaves.

## 4. How customers become Freshdesk contacts

Tickets need a requester. The workflow passes `requester_id`, so it resolves a contact
id for every phone number, in this order:

1. The workflow's own map (phone to contact id, in n8n workflow data).
2. `GET /api/v2/contacts?phone=<+e164>`: an exact filter on the list endpoint, not a
   search index, so a contact created a moment ago is found.
3. `GET /api/v2/contacts?mobile=<+e164>`: the same on the mobile field.
4. `POST /api/v2/contacts` with `{name, phone: "<+e164>"}`.

Steps 2 and 3 accept a hit only on an exact digit match, oldest contact first. A
contact found that way is used as it is: the workflow never edits a customer record you
already had and never sends an email address.

**Assumed:** the filters match the stored string exactly. The workflow writes and
searches the same `+<country code><number>` string, so contacts it created are always
found again. A contact whose number a person typed differently (spaces, dashes, a
national prefix) is not found, and the workflow creates a second contact for the same
person. Merge the two in Freshdesk if that happens; the workflow keeps using the one in
its map.

**Optional marker, off by default.** With `freshdesk_use_unique_external_id` set to
`true`, contacts the workflow creates also get `unique_external_id =
whatsapp:<+e164>`. It is a marker only: the contact list cannot filter on it, so the
workflow never looks contacts up by it. Freshdesk keeps the field unique, so a second
contact with the same marker is refused. Before turning it on, check that
`GET /api/v2/contact_fields` lists a field named `unique_external_id`; if not, an admin
enables "Unique External ID" in the contact fields settings (**assumed** label). A
refused create stops the execution with the reason.

**Privacy-ID chats.** WhatsApp hides some customers' numbers behind a privacy ID (an
"LID", chat id `<digits>@lid`); Spun reports such a chat with `peer_phone_e164: null`,
`peer_lid` and `identity_state: lid_unlinked` (`../README.md`, "WhatsApp privacy IDs
(LID)"). The digits are not a phone. Freshdesk cannot look a contact up by a custom
field or by `unique_external_id` (section 13), so such a chat is **create-only**:

- Step 1 checks the map key `lid:<digits>`; steps 2 and 3 are skipped (no phone).
- Step 4 creates the contact with the name (or `privacy ID <digits>`) and
  `unique_external_id = whatsapp-lid:<digits>`, no phone. Freshdesk needs at least one
  of email, phone, mobile or unique external id on a contact, so **privacy-ID chats need
  the Unique External ID contact field turned on** (check as above), whatever
  `freshdesk_use_unique_external_id` says. Without it the create is refused and the
  execution stops with that hint.
- The workflow map is the only dedupe. After a reset of the workflow's data, the next
  message of that chat is refused with `duplicate_value` (the marker is unique), and the
  execution names the contact to look at; no second contact is created.
- When Spun later learns the phone (`identity_state: lid_linked`) the workflow keeps
  using the same contact but does not add the phone to it (it never edits a contact);
  an agent can.

## 5. Ticket fields, group and other ticket values

**Spun chat id field (required).** Admin, Workflows, Ticket Fields (**assumed** menu
path): add a **single-line text** field labelled "Spun chat id". Do not mark it
required for agents or required when closing. Optionally add a second single-line text
field "Spun line" (it stores the Spun line that carried the chat, useful with several
WhatsApp lines) and a third, "Spun LID" (it stores the privacy ID of a privacy-ID chat,
section 4, so agents can see which chat a ticket belongs to).

Then read the API names: `GET {freshdesk_base}/api/v2/ticket_fields` and find your
fields by `label`. Their `name` is what the workflow needs, for example
`cf_spun_chat_id`, sometimes with a number at the end (verified: the name is derived
from the label and cannot be chosen). Paste it into `freshdesk_chat_id_field` (and the
line field's name into `freshdesk_line_field`, the Spun LID field's into
`freshdesk_lid_field`). A wrong name makes every ticket create
fail with `invalid_field`.

Agents should not edit the Spun chat id on a ticket: the workflow finds tickets by it.

| Setting | Default | Effect |
|---|---|---|
| `freshdesk_group_id` | placeholder (left out) | New WhatsApp tickets land in this group. The id is in the group's URL, or in `GET /api/v2/groups`. |
| `freshdesk_source` | `7` (Chat) | Ticket source. Freshdesk has no WhatsApp source value; reports that filter on Freshdesk's own WhatsApp channel do not include these tickets. Check the `source` field's choices in `GET /api/v2/ticket_fields` if your account uses custom sources. |
| `freshdesk_tags` | `["spun", "whatsapp"]` | Added to every new ticket. Keep `spun`: the automation rule and the fallback lookup rely on it. |
| `freshdesk_close_status` | `5` (Closed) | The status a Spun close sets. Use `4` (Resolved) if a message after a close should keep the same ticket instead of starting a new one. |
| `freshdesk_reopen_statuses` | `[3, 4]` (Pending, Resolved) | After an inbound note on a ticket in one of these statuses, the workflow sets it back to Open (2). Add custom statuses (6 and up) that should reopen too. |
| `freshdesk_lookup_days` | `365` | How far back the fallback ticket lookup reaches. The ticket list returns only the last 30 days unless told otherwise (verified). |

## 6. Reply URL and shared secret (agent notes to n8n)

1. Replace `PASTE-A-LONG-RANDOM-SUFFIX` in the **Helpdesk reply** node's path with a
   long random string, keeping the `freshdesk-reply-` prefix. The router matches the
   prefix, so no other node changes.
2. Activate the workflow and copy the production URL of **Helpdesk reply**
   (`.../webhook/freshdesk-reply-<your suffix>`).
3. Make a second long random string (32 characters or more) and paste it into
   `freshdesk_webhook_secret`. The automation rule in step 7 sends it in a custom
   header.

**Authenticity.** Freshdesk sends no signature header (verified). The adapter compares
the `X-Spun-Secret` header with `freshdesk_webhook_secret` in constant time and drops
anything that does not match. While the secret is still a placeholder, everything is
dropped. The ticket id in the body must be a number, so a call cannot point the
workflow at anything else.

**Delivery (verified).** Webhook calls are limited to 1,000 per hour per account; calls
over the limit are queued, not dropped. A call that does not get a 2xx answer is
retried every 30 minutes, up to 48 times, and account admins get an email. The Helpdesk
reply node answers 200 on receipt, so retries happen only when n8n is unreachable.
Duplicates are absorbed: each note is forwarded once, keyed on its conversation id.

## 7. The automation rule (agent notes to WhatsApp)

Admin, Workflows, Automations, **Ticket Updates** tab, **New rule** (verified location),
name "Spun: agent note to WhatsApp".

| Part | Value |
|---|---|
| When an action is performed by | **Agent**. Optionally limit it to your human agents; the workflow drops the integration agent's own notes either way, so the limit only saves calls. |
| Involves any of these events | **Note is added** and **Reply is sent**. |
| On tickets with these properties | **Tags** contains `spun` (or the Spun chat id field is not empty). |
| Perform these actions | **Trigger webhook**: request type **POST**, callback URL from step 6, no authentication, custom header `X-Spun-Secret: <your secret>`, encoding **JSON**, content **Advanced** with the body below. |

```json
{"kind": "note", "ticket_id": "{{ticket.id}}"}
```

The body carries the ticket id only: Freshdesk has no placeholder for the id or author
of the note that fired the rule, and the documentation does not say that placeholders
are JSON-escaped, so a body that embedded the note text could break on a quote or a
line break. On each call the workflow reads the ticket's conversations
(`GET /api/v2/tickets/{id}/conversations`, 100 per page) and forwards the notes and
replies that are:

- public (`private: false`) and not on the customer side (`incoming: false`),
- a note (source 2) or a reply (source 0),
- not written by the integration agent,
- created since the workflow started tracking the ticket (two minutes of slack, at most
  `dedupe_hours` back), or within `reply_window_minutes` (default 15) for a ticket the
  workflow does not track.

Oldest first, each one once; the conversation id is the idempotency key on Spun's
`send-message`.

**A ticket the workflow does not know.** When the workflow has no record of the ticket,
for example after its data was reset, the notes picked up on that first call are not
forwarded: the chat id is read from the ticket, but `SPUN: send-message` takes it from
the item `Chat id resolved?` handed on, which is still empty on that branch, so Spun
refuses the send, and the notes are still marked as handled, so they are not retried.
`Read chat id from ticket` remembers the ticket and starts tracking it from that moment,
the same way a ticket found on a customer message is tracked, so later notes are
forwarded as usual. Add a dropped note again if it must reach the customer.

**Loop guard.** Notes the workflow writes with its own key are agent actions and fire
this rule too (Freshdesk documents that requests made through its API by a webhook count as agent updates; the same is taken for the workflow's own writes). They are
dropped because they are written by the integration agent, the inbound ones are also
`incoming`, and the ones copied from Spun are private.

Verified: the Ticket Updates tab, performer Agent, the Trigger webhook action with
request type, JSON encoding, Advanced content, custom headers, and the placeholder
`{{ticket.id}}`; the event "Reply is sent". **Assumed:** the event label "Note is
added" (it is in Freshdesk's event list but not in the pages read for `API-NOTES.md`),
and that the event fires for public and private notes alike (the workflow ignores
private ones).

### Answer with public notes (important)

Tell your agents: on a WhatsApp ticket, answer with **Add note** and switch the note to
**public** (notes are private by default, verified). A public note goes to the customer
on WhatsApp; a private note stays internal. The **Reply** button sends an email, and a
customer the workflow created has no email address. What Freshdesk does with a reply to
a requester without email is not documented; if it saves the reply, the rule forwards it
like a note, but do not rely on it.

### Requester notifications

Inbound messages after the first are public notes made by the integration agent.
Freshdesk's requester email notifications may treat them, and the agents' public notes,
as agent comments and email the requester. Contacts the workflow creates have no email
and receive nothing; a contact you already had (found in step 4) may have one. Check
the requester notifications in Admin, Workflows, Email Notifications during the first
test and turn off the ones that would copy WhatsApp messages by email. **Assumed**: the
notification names and defaults were not checked.

## 8. Desk-side close and reopen (optional)

When `close_in_spun_when_desk_closes` is true, closing a ticket in Freshdesk archives the
conversation in Spun through `POST /api/integrations/conversations/close` (reason
`closed_in_freshdesk`), and reopening it calls `/reopen` (reason
`reopened_in_freshdesk`). Two more rules on the Ticket Updates tab drive it, with the
same webhook action, URL and header as step 7:

| Rule | Performed by | Event | Condition | Body |
|---|---|---|---|---|
| Spun: ticket closed | Agent | Status is changed from any status to **Closed** | Tags contains `spun` | `{"kind": "closed", "ticket_id": "{{ticket.id}}"}` |
| Spun: ticket reopened | Agent | Status is changed from **Closed** to any status | Tags contains `spun` | `{"kind": "reopened", "ticket_id": "{{ticket.id}}"}` |

If your team resolves tickets instead of closing them, use **Resolved** in both rules.

Requirements and caveats:

- The Spun API key needs the `manage_conversations` scope ("Close & Reopen
  Conversations" under Manage API Keys). A 403 from Spun means the key lacks it; a 404
  means no conversation matched.
- The workflow finds the conversation through its own ticket map. A ticket closed after
  the workflow's data was reset is not closed in Spun until a new message for it has
  passed through.
- The workflow's own status changes fire these rules too. They are skipped because the
  workflow's map already shows the ticket in that state, and Spun's own close and reopen
  events carry the workflow's reason (`desk_close_reason`, `desk_reopen_reason`).
- Freshdesk sends no delivery id, so the close and reopen keys are built from the ticket
  id, the kind and a 10-minute time bucket.
- Spun to Freshdesk always works: `conversation.closed` sets `freshdesk_close_status`
  (Closed by default), `conversation.reopened` sets Open. A ticket field marked required
  when closing makes Freshdesk refuse the close; the ticket then stays open in Freshdesk
  and in the workflow's map, and the execution shows the error. **Assumed:** Freshdesk
  accepts `status: 2` on a Closed ticket through the API.

## 9. Spun side

1. Webhooks page, Manage API Keys: create a key with `send_message` (and
   `manage_conversations` for step 8). Copy it once; create the n8n Header Auth
   credential `Authorization` = `Bearer wap_...` and select it on the three `SPUN:`
   nodes.
2. Webhooks page: create a subscription to the production URL of the **Spun events**
   node with the events `message.inbound`, `message.outbound`, `conversation.closed`,
   `conversation.reopened`, payload template **raw**, and a signing secret. Paste the
   secret into `spun_webhook_secret`.
3. Send a WhatsApp message to the line from another phone and follow the Delivery Log
   (Webhooks page) and the n8n executions list. The live-test checklist in
   `../README.md` lists the expected result of every step.

## 10. Settings node, all values

| Key | Value |
|---|---|
| `spun_api_base` | `https://api.spun.com/api/integrations` |
| `spun_webhook_secret` | the Spun subscription's signing secret (step 9) |
| `freshdesk_base` | step 1 |
| `freshdesk_webhook_secret` | step 6 |
| `freshdesk_chat_id_field`, `freshdesk_line_field`, `freshdesk_lid_field` | step 5 (the line and Spun LID fields are optional) |
| `freshdesk_group_id` | step 5, optional |
| `freshdesk_tags` | `["spun", "whatsapp"]`; keep `spun` |
| `freshdesk_source` | `7` (Chat) |
| `freshdesk_close_status` | `5` (Closed), or `4` (Resolved) |
| `freshdesk_reopen_statuses` | `[3, 4]` |
| `freshdesk_use_unique_external_id` | `false`; step 4 |
| `freshdesk_note_as_customer` | `false`. When `true`, inbound notes are sent with `user_id` = the customer's contact id, so the thread shows the customer as author. **Assumed** to work: the documentation describes `user_id` as "agent/user", and an account that rejects it fails each later message until it is set back to `false`. Check it in the live run before turning it on. |
| `freshdesk_lookup_days` | `365` |
| `desk_integration_user_id` | step 2 (`GET /api/v2/agents/me`) |
| `reply_window_minutes` | `15`: how old a note may be when the rule call arrives for a ticket the workflow does not track |
| `skip_groups` | `true`. Groups and newsletters always end at "Skipped (group or newsletter)" (no single person to make a contact for); privacy-ID chats are handled (section 4) |
| `inbound_prefix` | `[WhatsApp]` (also the echo filter) |
| `outbound_prefix` | `[Sent from Spun]` (private notes) |
| `desk_close_reason`, `desk_reopen_reason` | `closed_in_freshdesk`, `reopened_in_freshdesk` (loop guard for step 8) |
| `close_in_spun_when_desk_closes` | `false` until step 8 is set up and its first close is checked |
| `dedupe_hours` | `24` |

## 11. Message mapping

| Spun | Freshdesk |
|---|---|
| First `message.inbound` of a conversation | `POST /api/v2/tickets`: subject "WhatsApp: " plus the first line (80 characters), description `[WhatsApp] Name (+number): text` (`(privacy ID <digits>)` for a privacy-ID chat with no known phone), requester = the customer's contact, source 7, status Open, priority Low, tags, the Spun chat id field, optional group, line and Spun LID field. |
| Later `message.inbound` | `POST /api/v2/tickets/{id}/notes` with `private: false`, `incoming: true` and the same `[WhatsApp] Name (+number): text` body. A ticket in Pending or Resolved (`freshdesk_reopen_statuses`) is then set back to Open. |
| Message after the ticket was Closed | A new ticket; its description ends with "Previous ticket: #N". |
| Media | The same text with the file type, size and a link valid for 24 hours. Not uploaded as a Freshdesk attachment. |
| `message.outbound` sent from the Spun inbox | Private note `[Sent from Spun] Agent name: text`. |
| `conversation.closed` / `reopened` | Status `freshdesk_close_status` / Open. |
| Public agent note (or a saved reply) in Freshdesk | WhatsApp text message to the customer; HTML is converted to plain text; `freshdesk:<conversation id>` is the idempotency key. |
| Private note, customer-side note, or a note by the integration agent | Not sent. |

Note and description bodies are HTML: the text is escaped, line breaks become `<br>`
and links are clickable. Verified.

## 12. Freshdesk API usage and limits (verified figures)

- A new conversation costs up to 5 requests (phone filter, mobile filter, create
  contact, ticket list, create ticket), 3 for a privacy-ID chat with no known phone
  (no filters). A later message costs 2 (ticket read and note),
  plus 1 when the ticket goes back to Open. An agent note costs 1 request per 100
  conversations on the ticket, plus 1 ticket read when the workflow's map lost the
  ticket. A message sent from Spun costs 1, a close or reopen 1.
- Account limits per minute, shared with every other app and agent on the account
  (invalid requests count too):

  | Plan | Total | Ticket create | Ticket update | Tickets list | Contacts list |
  |---|---:|---:|---:|---:|---:|
  | Trial | 50 | | | | |
  | Growth | 100 | 50 | 50 | 40 | 40 |
  | Pro | 400 | 160 | 160 | 100 | 100 |
  | Enterprise | 700 | 280 | 280 | 200 | 200 |

  A refused request (429, with `Retry-After`) fails its n8n execution, visible in the
  execution list; the message stays in Spun. Re-run the execution in n8n after a minute.
  The workflow does not wait and retry on its own.
- Webhook calls: 1,000 per hour per account, then queued (step 6).
- The fallback ticket lookup reads the customer's 100 most recently updated tickets of
  the last `freshdesk_lookup_days`. Deleted and spam tickets are not listed, so a
  message for such a ticket starts a new one.
- The workflow reads at most 20 pages (2,000 notes and replies) per ticket. On a longer
  ticket new agent notes may not be found; close the conversation in Spun so the next
  message opens a new ticket.
- Attachments on a ticket are capped at 20 MB in total; the workflow sends media as
  links, so it never uploads.

## 13. What was verified and what was assumed

| Item | Status |
|---|---|
| Basic auth with the API key as user and `X` as password; the key carries the agent's role | Verified (re-checked 10/07) |
| `GET /api/v2/agents/me` for any agent; its `id` | Verified (re-checked 10/07) |
| The agent id equals the `user_id` on that agent's notes | Assumed |
| Ticket scope Global Access needed to see all tickets | Assumed |
| Contact list filters `phone`, `mobile` (exact), empty answer `[]` | Verified (re-checked 10/07) |
| The contact list has no `unique_external_id` filter (API-NOTES 2.1 listed one; corrected here) | Verified (re-checked 10/07) |
| Phone filters match the stored string exactly | Assumed |
| Contact create with `name`, `phone`, optional `unique_external_id` (unique, 409 on duplicate) | Verified |
| Contact create with `name` and `unique_external_id` only (privacy-ID chat, no phone) | Built on the verified create (one of email, phone, mobile, twitter id or unique external id is required); not live-tested |
| Ticket create with `requester_id`, `subject`, `description` (HTML), `source`, `status`, `priority`, `tags`, `group_id`, `custom_fields` | Verified |
| No WhatsApp ticket source; 7 = Chat | Verified |
| Custom field API names from `GET /api/v2/ticket_fields`, unknown names refused with `invalid_field` | Verified |
| Ticket list by `requester_id` with `updated_since`, 30-day default, deleted and spam excluded, `per_page` up to 100 | Verified (re-checked 10/07); the combination of `requester_id` and `updated_since` in one call is assumed |
| Ticket list objects carry `custom_fields`; `tags` are not in the list sample (the workflow checks the tag only when present) | Verified (re-checked 10/07) |
| View a ticket returns `tags`, `custom_fields`, `spam`, `deleted` | Verified (re-checked 10/07) |
| Notes: `private` defaults to true, `incoming` defaults to false, `user_id` "agent/user who is adding the note" | Verified (re-checked 10/07) |
| A note with `user_id` = the customer (`freshdesk_note_as_customer`) | Assumed; off by default |
| Conversations list: notes and replies, `source` 0 reply / 2 note, `per_page` up to 100, sort order not documented (sorted in the workflow) | Verified; the page loop's stop rule (a page with fewer than 100 items) is assumed |
| Status values 2 Open, 3 Pending, 4 Resolved, 5 Closed; `required_for_closure` blocks a close | Verified |
| `status: 2` accepted on a Closed ticket | Assumed |
| Trigger webhook action in Ticket Updates rules, Growth plan and up, custom headers, JSON, Advanced content, `{{ticket.id}}` | Verified (re-checked 10/07) |
| Event "Reply is sent" | Verified |
| Event "Note is added" label, and that it fires for public notes | Assumed |
| No webhook signature; 1,000 calls per hour; retried every 30 minutes, 48 times | Verified (re-checked 10/07) |
| API writes count as agent actions and fire Ticket Updates rules | Documented for webhook-made requests; taken to hold for the workflow's own writes (the filter drops them either way) |
| Placeholders are not JSON-escaped | Not documented; the body carries ids only |
| Requester email notifications on agent-made public notes | Assumed; see step 7 |
| Rate limits per plan, `Retry-After` on 429, invalid requests count | Verified (re-checked 10/07) |
| 20 MB attachment total per ticket | Verified (re-checked 10/07) |
