---
name: integrating-communiqueue
description: Use when an application needs to send email or signed webhooks through CommuniQueue (api.communiqueue.com), when wiring the X-Api-Key send or preview call, when authoring or editing a CommuniQueue template with Handlebars variables, or when a send returns an errorCode such as sender.not_verified or notifications.no_live_version.
---

# Integrating CommuniQueue

CommuniQueue is a templated notification API. The app never sends HTML: it POSTs a
template id plus string variables, and the live version of that template renders and
delivers. Changing the wording is a publish in CommuniQueue, not a deploy of the app.

Ids are GUIDs. The workspace id is `tenantId` and comes from the app URL
`https://app.communiqueue.com/workspaces/{workspaceId}`. The template id is the last
segment of `.../projects/{projectId}/templates/{templateId}`.

## Setup order

1. Workspace exists. Create a project, then a template inside it. Write the body in the
   editor with `{{variable}}` placeholders. Drafts autosave.
2. Verify a sender at `/workspaces/{id}/settings/senders`: a domain (three CNAMEs) or a
   single address. A managed send from an unverified from address is refused with
   `sender.not_verified` (422). Workspaces on their own provider skip this.
3. Test-send (the editor sends your draft; the template page sends the live version once
   published), then **Publish**. A template with no published version
   answers `notifications.no_live_version` (400).
4. Create an API key at `/workspaces/{id}/api-keys`. Shown once. Scoped to that workspace.
5. Store `COMMUNIQUEUE_API_KEY` and `COMMUNIQUEUE_WORKSPACE_ID` as secrets, and each
   template id as per-environment config.

## The send call

```
POST https://api.communiqueue.com/api/v1/notifications/send
X-Api-Key: cq_live_...
Content-Type: application/json
```

```json
{
  "tenantId": "<workspace guid>",
  "templateId": "<template guid>",
  "tags": [{ "key": "firstName", "value": "Ada" }],
  "emailOptions": { "to": ["ada@example.com"] }
}
```

- `tags` is required, may be `[]`, and every value is a **string**. Keys match
  `{{firstName}}` case-insensitively. Max 100 tags, 8,192 chars per value.
- `202 Accepted` means queued, not delivered. Keep the `correlationId` in your logs;
  delivery status appears in the workspace reports.
- `emailOptions.to/cc/bcc` are **added to** the template's default recipients.
  `overrideRecipients` **replaces** them. A send with no recipients from either source
  still returns 202 and then fails in the pipeline as "No recipients resolved".
- Omit `versionNumber` so every send uses whatever is live. Pin it only for frozen content.
- Retry only on `429` and `5xx`, with backoff. Every other 4xx is a request or setup fault.

One wrapper per app, both shown in `examples.md` (Node and C#).

## Errors

Every error is `application/problem+json` with an `errorCode`. Branch on the code, not
the message. Validation failures carry the umbrella `notifications.validation_failed` plus
an `errorCodes` array with the specific rules. `413` and `429` arrive with no code.

| Code | Status | Fix |
|---|---|---|
| `api_keys.invalid` / `api_keys.missing` | 401 | Header missing, key revoked or expired |
| `api_keys.tenant_mismatch` | 403 | Key belongs to a different workspace than `tenantId` |
| `notifications.template_not_found` | 404 | Wrong template GUID or wrong workspace |
| `notifications.no_live_version` | 400 | Publish the template |
| `sender.not_verified` | 422 | Verify the domain or single sender |
| `notifications.monthly_limit_exceeded` | 429 | Account delivery allowance used up |
| `notifications.webhook_destination_required` | 400 | Webhook template sent without `urlOverride` |

Full table, rate limits, preview response shape, webhook signature check: `reference.md`.

## Preview before sending

`POST /api/v1/notifications/preview` with the same body renders without delivering and
returns the subject, body, resolved recipients, and `variableMetadata` with
`missingVariables` and `unusedVariables`. Use it in CI to catch a renamed variable.
`GET /api/v1/api-keys/validate` checks a key and returns 200 with `isValid` either way.

## Writing a template

Body is HTML (visual or code editor), plus subject, optional `htmlHead`, from address and
name. Handlebars syntax:

| Write | Renders |
|---|---|
| `{{firstName}}` | HTML-escaped value |
| `{{{htmlSnippet}}}` | raw value, unescaped |
| `{{formatDate dueDate "MMMM d, yyyy"}}` | .NET date format of a parseable date string |
| `{{now "yyyy-MM-dd"}}` | current UTC time |
| `{{uppercase code}}` / `{{lowercase email}}` | case change |
| `{{#ifEquals plan "pro"}}...{{else}}...{{/ifEquals}}` | string equality branch |

`if`, `unless`, `with`, `lookup` and `log` also work. Tag values are flat strings, so
`#each` has nothing to iterate; put a list in the template as separate tags or as a
pre-rendered raw `{{{listHtml}}}`. A missing tag renders empty; preview reports it.
Each rendered part is capped at 1 MB.

Email type on the version: **Transactional** sends one message to all recipients;
**Marketing** sends one message per `to` recipient, adds List-Unsubscribe headers, and
honours project opt-outs. Open and click tracking are per-version toggles.

Recipients set on the project, container, or template are defaults that every send
inherits, so a template that always goes to the same inbox needs no `emailOptions`.

## Webhook templates

A Webhook template posts its rendered body to `webhookOptions.urlOverride` (https on 443,
public host, redirects not followed) with up to 16 custom headers. Every delivery is
signed: verify `X-CommuniQueue-Signature` over `{timestamp}.{deliveryId}.{rawBody}`
with HMAC-SHA256 and the workspace signing secret, reject old timestamps, dedupe on
`X-CommuniQueue-Delivery-Id`. Code in `reference.md`. Requires the Pro plan or above.

## Common mistakes

- Sending the template **name** or a slug as `templateId`. It is a GUID.
- Passing a number or boolean as a tag value. Values are strings; convert first.
- Treating 202 as delivered, then not looking at reports when mail is missing.
- Expecting `to` to replace the template defaults. Use `overrideRecipients` for that.
- Building an unsubscribe link by hand for marketing mail. Set the email type instead.
- Retrying a 400 or 422. Nothing changes until the request or the setup changes.
- Expecting 401 from `/api-keys/validate` on a bad key. It returns 200 with `isValid: false`.
