Skip to main content

Macros, Tags & Routing

Canned replies with placeholders and side-effect actions, a tag vocabulary, and a first-match-wins routing engine with a dry-run evaluator.

Overview

Three operational primitives sit behind the support desk:

  • Macros — canned replies. A macro carries a body with {{placeholder}} variables and, optionally, actions that are applied besides sending the text: set a status, add tags, assign a user.
  • Tags — your tenant's tag vocabulary for tickets: a name, a display colour and a description.
  • Routing rules — an ordered, first-match-wins triage list. Each rule ANDs a set of conditions over a fixed six-field allowlist and names what should happen when it matches (assignee, priority, tags). Rules are applied when a ticket is created (see creating a ticket), and the dry run tells you which one would match without writing anything.

Authentication

Authorization: Bearer cm_your_api_key
OperationScope
Every list/read, plus POST /routing-rules/evaluatesupport_ops:read
Every create, update, delete, plus POST /macros/{id}/usesupport_ops:write

POST /routing-rules/evaluate is a dry run — it writes nothing, so it needs only the read scope. POST /macros/{id}/use increments a counter, so it needs write.


Macros

The macro object

{
  "id": "9b8a…",
  "tenant_id": "a0b1…",
  "name": "Refund acknowledged",
  "body": "Hi {{customer_name}}, your refund for order {{order_id}} is on its way.",
  "actions": { "set_status": "pending", "add_tags": ["refund"], "assign_to": null },
  "category": "billing",
  "is_active": true,
  "usage_count": 41,
  "created_at": "2026-08-01T10:00:00Z",
  "updated_at": "2026-08-16T14:00:00Z"
}

actions

FieldTypeConstraints
set_statusstring | nullAt most 32 characters, lowercase identifier
add_tagsstring[] | nullAt most 20, each at most 64 characters, non-blank
assign_toUUID | nullMust be a user in your tenant

set_status must match ^[a-z][a-z0-9_-]{0,31}$; it is stored as written, not checked against the ticket status list, so use one of the ticket statuses if you intend to apply it to a ticket.

Unknown keys inside actions are rejected with 422 rather than ignored, so a typo cannot silently do nothing.

GET /api/v1/support/ops/macros

Ordered by usage_count descending, then name — so the picker shows what your team actually uses.

ParameterTypeConstraints
categorystringAt most 64 characters, exact match
is_activeboolean
limitinteger1 <= limit <= 200, default 50
offsetinteger0 <= offset <= 100000, default 0
curl "https://api.callmissed.com/api/v1/support/ops/macros?category=billing&is_active=true" \
  -H "Authorization: Bearer cm_your_api_key"

POST /api/v1/support/ops/macros

FieldTypeRequiredConstraints
namestringYes1–255 characters, not blank, unique per tenant
bodystringYes1–20,000 characters, not blank
actionsobjectNoSee above
categorystringNoAt most 64 characters
is_activebooleanNoDefault true
curl -X POST https://api.callmissed.com/api/v1/support/ops/macros \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Refund acknowledged",
    "body": "Hi {{customer_name}}, your refund for order {{order_id}} is on its way.",
    "actions": { "set_status": "pending", "add_tags": ["refund"] },
    "category": "billing"
  }'

Returns 201 with the macro object. 409 A macro named '…' already exists on a duplicate name; 404 Assignee not found in this tenant when actions.assign_to is not one of your users.

PATCH / DELETE /api/v1/support/ops/macros/{macro_id}

PATCH takes the same fields, all optional, and returns the updated macro; sending actions: null clears the actions. DELETE returns 204. Both return 404 Macro not found for an id outside your tenant.

curl -X PATCH https://api.callmissed.com/api/v1/support/ops/macros/{macro_id} \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'

curl -X DELETE https://api.callmissed.com/api/v1/support/ops/macros/{macro_id} \
  -H "Authorization: Bearer cm_your_api_key"

POST /api/v1/support/ops/macros/{macro_id}/use

Renders the macro against a context and bumps usage_count.

FieldTypeRequiredConstraints
contextobjectNoDefault {}. At most 50 keys; at most 16,000 characters serialised
curl -X POST https://api.callmissed.com/api/v1/support/ops/macros/9b8a…/use \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "context": { "customer_name": "Asha", "order_id": "A-4419" } }'
{
  "id": "9b8a…",
  "name": "Refund acknowledged",
  "body": "Hi Asha, your refund for order A-4419 is on its way.",
  "actions": { "set_status": "pending", "add_tags": ["refund"] },
  "usage_count": 42
}

Placeholder syntax is {{name}} (surrounding spaces allowed), where the name starts with a letter or underscore and may contain letters, digits, _ and ., up to 64 characters. Substituted values are truncated to 500 characters and are not re-scanned for placeholders.

Unknown or null-valued placeholders are left verbatim — {{order_id}} stays in the text rather than becoming an empty gap. Check the rendered body before sending it to a customer, or supply every variable the macro declares.

Applying the returned actions is your job: this endpoint renders and counts, it does not mutate a ticket.


Tags

{
  "id": "7f6e…",
  "tenant_id": "a0b1…",
  "name": "refund",
  "color": "#c2410c",
  "description": "Money going back to the customer",
  "created_at": "2026-08-01T10:00:00Z",
  "updated_at": "2026-08-01T10:00:00Z"
}
MethodPathScope
GET/api/v1/support/ops/tagssupport_ops:read
POST/api/v1/support/ops/tagssupport_ops:write
PATCH/api/v1/support/ops/tags/{tag_id}support_ops:write
DELETE/api/v1/support/ops/tags/{tag_id}support_ops:write

GET is ordered by name and takes limit (1..500, default 100) and offset (0..100000). There are no filters.

FieldTypeRequiredConstraints
namestringYes1–64 characters, not blank, unique per tenant
colorstringNoA hex triplet such as #c2410c or #c40. Stored lowercase
descriptionstringNoAt most 255 characters

Anything other than a hex triplet returns 422 color must be a hex triplet like '#aabbcc'. PATCH takes the same fields, all optional, and returns the updated tag; DELETE returns 204. 404 Tag not found for an id outside your tenant, 409 A tag named '…' already exists on a duplicate name.

curl "https://api.callmissed.com/api/v1/support/ops/tags?limit=100" \
  -H "Authorization: Bearer cm_your_api_key"

curl -X POST https://api.callmissed.com/api/v1/support/ops/tags \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "refund", "color": "#c2410c", "description": "Money going back to the customer" }'

curl -X PATCH https://api.callmissed.com/api/v1/support/ops/tags/{tag_id} \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "color": "#15803d" }'

curl -X DELETE https://api.callmissed.com/api/v1/support/ops/tags/{tag_id} \
  -H "Authorization: Bearer cm_your_api_key"

Routing rules

Vocabulary

ConceptAllowed values
Condition fieldchannel, priority, status, subject_contains, contact_email_domain, tag
Condition operatoreq, neq, contains, in, is_set, is_not_set
assign_strategydirect, round_robin

The field list is a closed allowlist — an unknown field is 422, not a silently-false condition.

Condition fieldRead from (in a dry run)
channelchannel
prioritypriority
statusstatus
subject_containssubject
contact_email_domainThe part of contact_email after @, lowercased
tagtags — eq means "carries this tag", neq "does not carry it", contains "some tag contains the text"

All comparisons are case-insensitive and ignore surrounding whitespace.

Condition value rules

Operatorvalue
inA non-empty array, at most 100 entries
is_set / is_not_setOmitted
Everything elseA non-blank scalar (string at most 512 characters, number or boolean)

The rule object

{
  "id": "2c3d…",
  "tenant_id": "a0b1…",
  "name": "Enterprise urgent → Ravi",
  "position": 0,
  "conditions": [
    { "field": "priority", "operator": "eq", "value": "urgent" },
    { "field": "contact_email_domain", "operator": "in", "value": ["acme.com", "globex.com"] }
  ],
  "assign_to_user_id": "b1f2…",
  "assign_strategy": "direct",
  "set_priority": "urgent",
  "add_tags": ["enterprise"],
  "is_active": true,
  "created_at": "2026-08-05T09:00:00Z",
  "updated_at": "2026-08-05T09:00:00Z"
}

All conditions on a rule are ANDed. Rules are evaluated in position order and the first match wins — position is the policy, so the list is never re-sorted for you.

GET /api/v1/support/ops/routing-rules

Returns an array of rule objects in evaluation order (position, then creation time). Takes is_active, limit (1..500, default 100) and offset (0..100000, default 0).

curl "https://api.callmissed.com/api/v1/support/ops/routing-rules?is_active=true" \
  -H "Authorization: Bearer cm_your_api_key"

POST /api/v1/support/ops/routing-rules

FieldTypeRequiredConstraints
namestringYes1–255 characters, unique per tenant
conditionsobject[]NoDefault []. At most 20 per rule
positionintegerNo0 <= position <= 100000, default 0
assign_to_user_idUUIDNoMust be a user in your tenant
assign_strategystringNodirect (default) or round_robin
set_prioritystringNoAt most 16 characters, lowercase identifier
add_tagsstring[]NoAt most 20, each at most 64 characters. De-duplicated
is_activebooleanNoDefault true

A rule with an empty conditions array matches everything — use it deliberately as a final catch-all at the highest position.

round_robin is stored on the rule for your own assignment logic; the dry run below has no team roster to rotate through, so it returns the rule's assign_to_user_id for both strategies.

curl -X POST https://api.callmissed.com/api/v1/support/ops/routing-rules \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Enterprise urgent → Ravi",
    "position": 0,
    "conditions": [
      { "field": "priority", "operator": "eq", "value": "urgent" },
      { "field": "contact_email_domain", "operator": "in", "value": ["acme.com", "globex.com"] }
    ],
    "assign_to_user_id": "b1f2c3d4-1111-2222-3333-444455556666",
    "add_tags": ["enterprise"]
  }'

Returns 201 with the rule object. 409 A routing rule named '…' already exists on a duplicate name; 404 Assignee not found in this tenant when assign_to_user_id is not one of your users.

PATCH /api/v1/support/ops/routing-rules/reorder

Rewrites the whole evaluation order.

FieldTypeRequiredConstraints
rule_idsUUID[]Yes1–500 entries. Must be every routing rule in your tenant, exactly once
curl -X PATCH https://api.callmissed.com/api/v1/support/ops/routing-rules/reorder \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "rule_ids": ["2c3d…", "8e9f…", "4a5b…"] }'

Returns the rules in their new order. New position is the array index. Duplicate ids return 422 rule_ids must not contain duplicates. A partial list returns 422 rule_ids must list every routing rule in this tenant exactly once — reordering is all-or-nothing, so two concurrent partial reorders cannot interleave into a nonsense order.

POST /api/v1/support/ops/routing-rules/evaluate

A dry run. Nothing is written; you get back what would happen.

FieldTypeConstraints
channelstringAt most 64 characters
prioritystringAt most 32 characters
statusstringAt most 32 characters
subjectstringAt most 1,000 characters
contact_emailstringAt most 320 characters
tagsstring[]At most 100 entries

Every field is optional. An absent fact fails any eq, contains, in or is_set condition that asks about it (it satisfies neq and is_not_set) — so send the whole picture when testing.

curl -X POST https://api.callmissed.com/api/v1/support/ops/routing-rules/evaluate \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "priority": "urgent",
    "subject": "Cannot check out",
    "contact_email": "[email protected]",
    "tags": ["vip"]
  }'
{
  "matched": true,
  "rule_id": "2c3d…",
  "rule_name": "Enterprise urgent → Ravi",
  "assign_to_user_id": "b1f2…",
  "assign_strategy": "direct",
  "set_priority": "urgent",
  "add_tags": ["enterprise"]
}

Only is_active: true rules are considered. When nothing matches you get {"matched": false, …, "add_tags": []} — a miss, not an error.

The same decision is applied automatically when a ticket is created with POST /api/v1/support/tickets: the rule fills in the assignee only when the request names none, sets the priority only when the request sends none, and adds its tags to the request's. Tickets that already exist are not re-routed — change them with PATCH /api/v1/support/tickets/{ticket_id}.

PATCH / DELETE /api/v1/support/ops/routing-rules/{rule_id}

Same fields as create, all optional; sending conditions replaces the whole list. Returns the updated rule. DELETE returns 204. 404 Routing rule not found for an id outside your tenant.

curl -X PATCH https://api.callmissed.com/api/v1/support/ops/routing-rules/{rule_id} \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "is_active": false }'

curl -X DELETE https://api.callmissed.com/api/v1/support/ops/routing-rules/{rule_id} \
  -H "Authorization: Bearer cm_your_api_key"

Errors

StatusWhen
403Key is missing support_ops:read / support_ops:write
404Macro, tag, rule or assignee not in your tenant
409Duplicate macro, tag or rule name
422Blank name or body, over 20 conditions, an unknown condition field or operator, a value that does not fit its operator, a bad colour, an unknown key in actions, an incomplete or duplicated reorder list

Nothing on this page consumes credits.