---
title: "Create a manual suppression"
method: POST
path: "/email_blocks"
tags: ["Email Suppressions"]
---

# Create a manual suppression

`POST /email_blocks`

Creates a suppression with `reason: manual_block` and `source: manual`.
Caller-supplied `reason` / `source` are **ignored**; `scope` is
**derived** server-side from `domain_id` / `from` and is never
trusted. Idempotent: if a matching row already exists (NULL-safe
dedupe key: account_id, scope, to, reason, domain_id, from),
returns the existing record with `200` (no new audit event).

`bounce_category`, `dsn_code`, `meta`, and `group_id` are **not
accepted** on the public surface. Use the unsubscribe-group
suppression endpoint or the internal create surface for those.

## Request body

- CreateEmailBlockRequest — `reason`/`source` are forced to `manual_block`/`manual` (ignored if supplied). `scope` is derived. `bounce_category`/`dsn_code`/`meta`/ `group_id` are not accepted on the public surface.
  - `to` string, required — Recipient address (normalized: trim + lower-case).
  - `from` string, nullable — Sender address (normalized). `null` ⇒ account/domain scope.
  - `domain_id` string, uuid, nullable — `null` ⇒ account scope.
  - `expires_at` string, date-time, nullable

## Response `200`

Idempotent — matching suppression already existed.

- EmailBlockResponse
  - `data` EmailBlock, required — Suppression record. Schema fields hidden by the view: `account_id`, `bounce_category`, `dsn_code`, `meta`.
    - `id` string, uuid, required
    - `record_type` 'email_block', required — View-only discriminator.
    - `domain_id` string, uuid, nullable — `null` ⇒ account scope. Stored on the row; exposed here.
    - `group_id` string, uuid, nullable — `null` ⇒ global; set ⇒ group-scoped opt-out.
    - `from` string, nullable — `null` ⇒ not address-scope. (schema: from_address)
    - `to` string, required — Normalized recipient. (schema: to_address)
    - `reason` 'hard_bounce' | 'spam_complaint' | 'unsubscribe' | 'invalid' | 'manual_block', required
    - `source` 'feedback' | 'manual' | 'import' | 'system', required
    - `scope` 'account' | 'domain' | 'address', required — Derived server-side from `domain_id`/`from`; never trusted from the caller.
    - `status` 'active' | 'expired' | 'removed', required
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required
    - `expires_at` string, date-time, nullable

## Other responses

- `201` — Created.
- `401` — Missing or invalid gateway auth.
- `406` — Framework-rendered error (e.g. 406 Not Acceptable, 405 Method Not Allowed, 415 Unsupported Media Type). HTTP status matches the error and the body `code` carries that same status (e.g. `"406"`, not a hardcoded `"500"`). The explicit `500.json` clause still emits `code: "500"` for genuine 500s.
- `422` — Validation error (changeset or internal `Params`). One error object per field, `source.pointer /data/attributes/<field>`.

---

[API](https://skmtc.net/team-telnyx/apis/telnyx-api-2.md) · [All operations](https://skmtc.net/team-telnyx/apis/telnyx-api-2/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/team-telnyx/telnyx-api-2/revisions/8f5f4e537994/schema)
