{
  "openapi": "3.1.0",
  "info": {
    "title": "SNDR Partner API",
    "version": "1.0.0",
    "description": "OAuth 2.0 integration for partners — act on behalf of SNDR users to send emails and manage contacts, domains, templates, broadcasts, webhooks, and suppressions.\n\nCredentials are admin-issued (no self-service registration). Contact your SNDR account manager for a client_id and client_secret.\n\nFull documentation: https://sndr.sh/docs/cranl",
    "contact": {
      "name": "SNDR Partnerships",
      "url": "https://sndr.sh/docs/cranl"
    }
  },
  "servers": [
    {
      "url": "https://api.sndr.sh",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "OAuth",
      "description": "Authorization Code flow with optional PKCE."
    },
    {
      "name": "Me",
      "description": "Connected user, organization, plan, and the upgrade URL."
    },
    {
      "name": "Emails",
      "description": "Send transactional emails and retrieve delivery history."
    },
    {
      "name": "Analytics",
      "description": "Aggregate email performance metrics for the connected account."
    },
    {
      "name": "Domains",
      "description": "Add, verify, and remove sending domains on the connected account."
    },
    {
      "name": "Templates",
      "description": "Create and manage reusable email templates with variable interpolation."
    },
    {
      "name": "Contacts",
      "description": "Create, manage, and bulk-import subscriber contacts."
    },
    {
      "name": "Contact Groups",
      "description": "Organize contacts into segments for targeted broadcasts."
    },
    {
      "name": "Broadcasts",
      "description": "Create and send bulk email campaigns to contact groups."
    },
    {
      "name": "Webhooks",
      "description": "Register HTTP endpoints that receive real-time email events."
    },
    {
      "name": "Suppressions",
      "description": "Manage the suppression list that blocks emails to specific addresses."
    }
  ],
  "paths": {
    "/api/cranl/v1/me": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Get account",
        "description": "Returns the connected user, their organization (current plan slug and billing/upgrade URL), and the scopes granted to your token.",
        "operationId": "get_me",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "user": {
                    "id": "usr_5b2f8c1e-9a3",
                    "email": "founder@acme.com",
                    "name": "Alex Johnson",
                    "created_at": "2026-01-15T10:30:00Z"
                  },
                  "organization": {
                    "id": "org_8d41c7a2-3f6",
                    "name": "Acme Corp",
                    "plan": "pro",
                    "upgrade_url": "https://app.sndr.sh/billing"
                  },
                  "scopes": [
                    "read",
                    "send"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — The connected user or organization no longer exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/send": {
      "post": {
        "tags": [
          "Emails"
        ],
        "summary": "Send email",
        "description": "Queue an email for delivery. Returns 202 Accepted with the queued Email. Send an Idempotency-Key header to make retries safe: a repeat with the same key within 24 hours returns the first response.",
        "operationId": "post_send",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "string",
                    "description": "Sender, as an address or \"Name <address>\". Its domain must be added and verified on the connected account."
                  },
                  "to": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Recipient addresses, 1–1000, plain format (no display names). Must be an array, even for one recipient."
                  },
                  "cc": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "CC addresses, max 100."
                  },
                  "bcc": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "BCC addresses, max 100. to + cc + bcc together may not exceed 1000."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Subject line, single line, max 998 bytes. Required unless the template provides one."
                  },
                  "html": {
                    "type": "string",
                    "description": "HTML body. Send html, text or both, unless template_id provides them."
                  },
                  "text": {
                    "type": "string",
                    "description": "Plain-text body."
                  },
                  "reply_to": {
                    "type": "string",
                    "description": "Reply-To address."
                  },
                  "template_id": {
                    "type": "string",
                    "description": "Saved template ID (tpl_...). Its subject, html and text fill any of those fields you leave empty."
                  },
                  "variables": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Values for {{ name }} placeholders in subject, html and text, e.g. {\"first_name\":\"Alex\"}. {{ user.name }} reads nested objects; unknown placeholders are left as is."
                  },
                  "headers": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Extra headers as string pairs, e.g. {\"X-Entity-Ref-ID\":\"1234\"}. No CR/LF allowed; standard headers such as From, To, Subject, Reply-To or List-Unsubscribe are ignored."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "description": "Objects with string values, e.g. [{\"name\":\"category\",\"value\":\"welcome\"}]. Accepted but not stored or returned yet."
                  },
                  "attachments": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "description": "Up to 10 files as {filename, content, content_type?}. content is standard base64; content_type defaults to application/octet-stream. Max 10 MiB decoded in total; the whole request max 16 MiB."
                  }
                },
                "required": [
                  "from",
                  "to"
                ]
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "em_3f9c1a2b-7d4",
                  "organization_id": "org_8d41c7a2-3f6",
                  "from": "\"Acme\" <noreply@acme.com>",
                  "to": [
                    "customer@example.com"
                  ],
                  "subject": "Your order #1234 is confirmed",
                  "status": "queued",
                  "domain_id": "dom_a7c2e910-4b1",
                  "created_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body failed validation — e.g. to sent as a string, an invalid address, no subject or body, over 1000 recipients, CR/LF in a header field, unknown template_id, or a bad attachment.\nsuppressed — Every to address is on the suppression list. Suppressed cc/bcc addresses are dropped silently.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "quota_exceeded — The monthly email quota is used up. It resets on the 1st of the month (UTC).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.\ndomain_not_linked — The from domain has not been added to the connected account.\ndomain_not_verified — The from domain is added but its DNS records are not verified yet.\nquarantine_active — Sending is paused for this account. Contact SNDR support.\nforbidden — A recipient is on a known spam-trap list. Remove it and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large — The request, or the rendered html + text, is over 16 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "send_velocity — Sending too fast for this account. Back off and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "sends_frozen — Sending is temporarily paused platform-wide. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/emails": {
      "get": {
        "tags": [
          "Emails"
        ],
        "summary": "List emails",
        "description": "List emails sent from the connected account, newest first. Cursor-paginated: pass next_cursor back as cursor while has_more is true.",
        "operationId": "get_emails",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 50, max 200.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The next_cursor value from the previous page. Omit for the first page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "em_3f9c1a2b-7d4",
                      "organization_id": "org_8d41c7a2-3f6",
                      "from": "\"Acme\" <noreply@acme.com>",
                      "to": [
                        "customer@example.com"
                      ],
                      "subject": "Your order #1234 is confirmed",
                      "status": "delivered",
                      "domain_id": "dom_a7c2e910-4b1",
                      "created_at": "2026-06-01T12:00:00Z",
                      "delivered_at": "2026-06-01T12:00:04Z"
                    }
                  ],
                  "next_cursor": "eyJ0cyI6IjIwMjYtMDYtMDFUMTI6MDA6MDBaIiwiaWQiOiJlbV8zZjljMWEyYi03ZDQifQ",
                  "has_more": true,
                  "count": 1,
                  "limit": 1
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — limit is above 200, or cursor is not a value returned as next_cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/emails/{id}": {
      "get": {
        "tags": [
          "Emails"
        ],
        "summary": "Get email",
        "description": "Retrieve one email with its current status. error explains a failed or bounced email; message bodies are not returned.",
        "operationId": "get_emails_id",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Email ID (em_...), as returned by POST /send.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "em_3f9c1a2b-7d4",
                  "organization_id": "org_8d41c7a2-3f6",
                  "from": "\"Acme\" <noreply@acme.com>",
                  "to": [
                    "customer@example.com"
                  ],
                  "cc": [
                    "accounts@example.com"
                  ],
                  "subject": "Your order #1234 is confirmed",
                  "status": "delivered",
                  "domain_id": "dom_a7c2e910-4b1",
                  "created_at": "2026-06-01T12:00:00Z",
                  "delivered_at": "2026-06-01T12:00:04Z"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/analytics": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Get analytics",
        "description": "Counts emails created in the window by status; sent counts every email, whatever its status. inbox_rate is delivered / sent. Open and click tracking is not available.",
        "operationId": "get_analytics",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Window: 1d | 7d | 30d | 90d. Default 30d (also used for unknown values). The window used is returned as range.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "range": "7d",
                  "sent": 2140,
                  "delivered": 2118,
                  "bounced": 9,
                  "failed": 11,
                  "complained": 2,
                  "inbox_rate": 0.9897,
                  "as_of": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/domains": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "List domains",
        "description": "List all sending domains on the connected account with their status and DNS records. Not paginated.",
        "operationId": "get_domains",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "dom_a7c2e910-4b1",
                      "organization_id": "org_8d41c7a2-3f6",
                      "name": "mail.acme.com",
                      "status": "verified",
                      "region": "us-east-1",
                      "spf_status": "verified",
                      "dkim_status": "verified",
                      "dmarc_status": "verified",
                      "mx_status": "verified",
                      "tls_rpt_status": "failed",
                      "mta_sts_status": "failed",
                      "bimi_status": "failed",
                      "records": [
                        {
                          "type": "TXT",
                          "name": "mail.acme.com",
                          "value": "v=spf1 include:_spf.sndr.sh ~all",
                          "status": "verified",
                          "hint": "Add include:_spf.sndr.sh to your existing SPF record, or use this value if no SPF record exists. Keep one SPF record and retain your other mail providers.",
                          "required": true,
                          "label": "SPF"
                        }
                      ],
                      "created_at": "2026-01-20T09:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Add domain",
        "description": "Add a sending domain. Returns 201 with the domain (status pending) and the DNS records to publish; required ones have required: true.",
        "operationId": "post_domains",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Domain or subdomain to send from, e.g. mail.acme.com."
                  },
                  "region": {
                    "type": "string",
                    "description": "Region label stored on the domain. Defaults to us-east-1."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "dom_a7c2e910-4b1",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "mail.acme.com",
                  "status": "pending",
                  "region": "us-east-1",
                  "spf_status": "unknown",
                  "dkim_status": "unknown",
                  "dmarc_status": "unknown",
                  "mx_status": "unknown",
                  "tls_rpt_status": "unknown",
                  "mta_sts_status": "unknown",
                  "bimi_status": "unknown",
                  "records": [
                    {
                      "type": "TXT",
                      "name": "mail.acme.com",
                      "value": "v=spf1 include:_spf.sndr.sh ~all",
                      "status": "unknown",
                      "hint": "Add include:_spf.sndr.sh to your existing SPF record, or use this value if no SPF record exists. Keep one SPF record and retain your other mail providers.",
                      "required": true,
                      "label": "SPF"
                    },
                    {
                      "type": "TXT",
                      "name": "sndr._domainkey.mail.acme.com",
                      "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...",
                      "status": "unknown",
                      "hint": "DKIM signing key — every outgoing message is signed with it.",
                      "required": true,
                      "label": "DKIM"
                    },
                    {
                      "type": "TXT",
                      "name": "_dmarc.mail.acme.com",
                      "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@mail.acme.com; ruf=mailto:dmarc@mail.acme.com; pct=100; adkim=r; aspf=r",
                      "status": "unknown",
                      "hint": "Policy that ties SPF + DKIM together. Tells receivers what to do with messages that fail.",
                      "required": true,
                      "label": "DMARC"
                    }
                  ],
                  "created_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, or name is missing or not a valid domain name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit_reached — The plan’s domain limit is reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "conflict — This domain is already added to the connected account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/domains/{id}/verify": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Verify domain",
        "description": "Run the DNS checks now and return the updated domain. status becomes verified when SPF, DKIM and DMARC all pass, otherwise it stays pending; each check is verified or failed.",
        "operationId": "post_domains_id_verify",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Domain ID (dom_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "dom_a7c2e910-4b1",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "mail.acme.com",
                  "status": "pending",
                  "region": "us-east-1",
                  "spf_status": "verified",
                  "dkim_status": "verified",
                  "dmarc_status": "failed",
                  "mx_status": "verified",
                  "tls_rpt_status": "failed",
                  "mta_sts_status": "failed",
                  "bimi_status": "failed",
                  "records": [
                    {
                      "type": "TXT",
                      "name": "_dmarc.mail.acme.com",
                      "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@mail.acme.com; ruf=mailto:dmarc@mail.acme.com; pct=100; adkim=r; aspf=r",
                      "status": "failed",
                      "hint": "Policy that ties SPF + DKIM together. Tells receivers what to do with messages that fail.",
                      "required": true,
                      "label": "DMARC"
                    }
                  ],
                  "created_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/domains/{id}": {
      "delete": {
        "tags": [
          "Domains"
        ],
        "summary": "Delete domain",
        "description": "Remove a sending domain. Emails already sent are unaffected, but the domain can no longer be used as a from address.",
        "operationId": "delete_domains_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Domain ID (dom_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Success. No content."
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/templates": {
      "get": {
        "tags": [
          "Templates"
        ],
        "summary": "List templates",
        "description": "List all templates on the connected account, most recently updated first. Not paginated.",
        "operationId": "get_templates",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "tpl_c19e5d34-82a",
                      "organization_id": "org_8d41c7a2-3f6",
                      "name": "Order confirmation",
                      "subject": "Your order #{{ order_id }} is confirmed",
                      "html": "<h1>Hi {{ first_name }},</h1><p>Order #{{ order_id }} confirmed.</p>",
                      "text": "Hi {{ first_name }}, order #{{ order_id }} confirmed.",
                      "version": 3,
                      "created_at": "2026-02-10T14:00:00Z",
                      "updated_at": "2026-05-01T09:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Templates"
        ],
        "summary": "Create template",
        "description": "Create a template. Returns 201 with the template. HTML is sanitized on save: scripts, <style> blocks and event handlers are removed; inline styles stay.",
        "operationId": "post_templates",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Internal name (not shown to recipients), 1–200 characters."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Subject line, max 998 characters. Supports {{ variable }} placeholders."
                  },
                  "html": {
                    "type": "string",
                    "description": "HTML body. Either html or text is required."
                  },
                  "text": {
                    "type": "string",
                    "description": "Plain-text body."
                  }
                },
                "required": [
                  "name",
                  "subject"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "tpl_c19e5d34-82a",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "Order confirmation",
                  "subject": "Your order #{{ order_id }} is confirmed",
                  "html": "<h1>Hi {{ first_name }},</h1><p>Order #{{ order_id }} confirmed.</p>",
                  "text": "Hi {{ first_name }}, order #{{ order_id }} confirmed.",
                  "version": 1,
                  "created_at": "2026-06-01T12:00:00Z",
                  "updated_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, has an unknown field, or a field has the wrong type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit_reached — The plan’s template limit is reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — name or subject is missing or too long, or both html and text are empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/templates/{id}": {
      "get": {
        "tags": [
          "Templates"
        ],
        "summary": "Get template",
        "description": "Retrieve one template, including its html and text body.",
        "operationId": "get_templates_id",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Template ID (tpl_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "tpl_c19e5d34-82a",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "Order confirmation",
                  "subject": "Your order #{{ order_id }} is confirmed",
                  "html": "<h1>Hi {{ first_name }},</h1><p>Order #{{ order_id }} confirmed.</p>",
                  "text": "Hi {{ first_name }}, order #{{ order_id }} confirmed.",
                  "version": 3,
                  "created_at": "2026-02-10T14:00:00Z",
                  "updated_at": "2026-05-01T09:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Templates"
        ],
        "summary": "Update template",
        "description": "Update the fields you send; omitted fields keep their value. Empty html or text values are ignored, so a body cannot be cleared. Each update increments version.",
        "operationId": "patch_templates_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Template ID (tpl_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New internal name, 1–200 characters."
                  },
                  "subject": {
                    "type": "string",
                    "description": "New subject line, max 998 characters."
                  },
                  "html": {
                    "type": "string",
                    "description": "New HTML body (sanitized on save)."
                  },
                  "text": {
                    "type": "string",
                    "description": "New plain-text body."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "tpl_c19e5d34-82a",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "Order confirmation",
                  "subject": "Order #{{ order_id }} has shipped",
                  "html": "<h1>Hi {{ first_name }},</h1><p>Order #{{ order_id }} confirmed.</p>",
                  "text": "Hi {{ first_name }}, order #{{ order_id }} confirmed.",
                  "version": 4,
                  "created_at": "2026-02-10T14:00:00Z",
                  "updated_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, has an unknown field, or a field has the wrong type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — name or subject is empty or too long.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Templates"
        ],
        "summary": "Delete template",
        "description": "Permanently delete a template. Emails already sent are unaffected; later sends with this template_id fail with invalid_request.",
        "operationId": "delete_templates_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Template ID (tpl_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Success. No content."
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/contacts": {
      "get": {
        "tags": [
          "Contacts"
        ],
        "summary": "List contacts",
        "description": "List contacts on the connected account, newest first. Cursor-paginated; counts holds account-wide totals per status.",
        "operationId": "get_contacts",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 50, max 200.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The next_cursor value from the previous page. Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only contacts with this status: subscribed | unsubscribed | bounced | complained.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "group_id",
            "in": "query",
            "required": false,
            "description": "Only contacts in this group (grp_...).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive search in email, first name and last name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "con_0e6b7f21-c58",
                      "organization_id": "org_8d41c7a2-3f6",
                      "email": "jane@example.com",
                      "first_name": "Jane",
                      "last_name": "Doe",
                      "attributes": {
                        "plan": "pro",
                        "city": "Berlin"
                      },
                      "status": "subscribed",
                      "source": "checkout",
                      "created_at": "2026-03-01T10:00:00Z",
                      "updated_at": "2026-03-01T10:00:00Z"
                    }
                  ],
                  "has_more": false,
                  "count": 1,
                  "limit": 20,
                  "counts": {
                    "subscribed": 1,
                    "unsubscribed": 0,
                    "bounced": 0,
                    "complained": 0
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — limit is above 200, or cursor is not a value returned as next_cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Contacts"
        ],
        "summary": "Create contact",
        "description": "Create one contact. Returns 201 with the contact, or 409 if the email already exists — use POST /contacts/bulk to upsert.",
        "operationId": "post_contacts",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Email address. Stored lowercase and unique per account."
                  },
                  "first_name": {
                    "type": "string",
                    "description": "First name, max 100 characters."
                  },
                  "last_name": {
                    "type": "string",
                    "description": "Last name, max 100 characters."
                  },
                  "status": {
                    "type": "string",
                    "description": "subscribed (default), unsubscribed, bounced or complained. Only subscribed contacts receive broadcasts."
                  },
                  "attributes": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Free-form key-value data, e.g. {\"plan\":\"pro\"}."
                  },
                  "source": {
                    "type": "string",
                    "description": "Label for where the contact came from. Defaults to manual."
                  },
                  "group_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Groups (grp_...) to add the new contact to. Unknown IDs are ignored."
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "con_0e6b7f21-c58",
                  "organization_id": "org_8d41c7a2-3f6",
                  "email": "jane@example.com",
                  "first_name": "Jane",
                  "last_name": "Doe",
                  "attributes": {
                    "plan": "pro",
                    "city": "Berlin"
                  },
                  "status": "subscribed",
                  "source": "checkout",
                  "created_at": "2026-06-01T12:00:00Z",
                  "updated_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, has an unknown field, or a field has the wrong type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "conflict — A contact with this email already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — email is missing or invalid, a name is over 100 characters, or status is not one of the four values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/contacts/bulk": {
      "post": {
        "tags": [
          "Contacts"
        ],
        "summary": "Import contacts",
        "description": "Upsert up to 500 contacts by email. For existing contacts, attributes are merged and first_name, last_name and status are updated only when the row sets them, so an unsubscribed contact stays unsubscribed. New contacts default to subscribed.",
        "operationId": "post_contacts_bulk",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contacts": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "description": "Contacts as {email, first_name?, last_name?, status?, attributes?}, 1–500 per request."
                  },
                  "source": {
                    "type": "string",
                    "description": "Source label for newly created contacts. Defaults to manual."
                  },
                  "group_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Groups (grp_...) to add every contact in the batch to."
                  }
                },
                "required": [
                  "contacts"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "inserted": 1,
                  "updated": 1,
                  "skipped": 0
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, has an unknown field, or a field has the wrong type.\nchunk_too_large — More than 500 contacts in one request. Split the import.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — contacts is empty, or a row has a missing or invalid email or an unknown status (fields names each row, e.g. row.<index>.email). Nothing is saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/contacts/{id}": {
      "get": {
        "tags": [
          "Contacts"
        ],
        "summary": "Get contact",
        "description": "Retrieve one contact by ID.",
        "operationId": "get_contacts_id",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Contact ID (con_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "con_0e6b7f21-c58",
                  "organization_id": "org_8d41c7a2-3f6",
                  "email": "jane@example.com",
                  "first_name": "Jane",
                  "last_name": "Doe",
                  "attributes": {
                    "plan": "pro",
                    "city": "Berlin"
                  },
                  "status": "subscribed",
                  "source": "checkout",
                  "created_at": "2026-03-01T10:00:00Z",
                  "updated_at": "2026-03-01T10:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Contacts"
        ],
        "summary": "Update contact",
        "description": "Update the fields you send; omitted fields are unchanged. The email address cannot be changed.",
        "operationId": "patch_contacts_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Contact ID (con_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "first_name": {
                    "type": "string",
                    "description": "New first name, max 100 characters."
                  },
                  "last_name": {
                    "type": "string",
                    "description": "New last name, max 100 characters."
                  },
                  "status": {
                    "type": "string",
                    "description": "subscribed, unsubscribed, bounced or complained."
                  },
                  "attributes": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Replaces the whole attributes object (keys are not merged)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "con_0e6b7f21-c58",
                  "organization_id": "org_8d41c7a2-3f6",
                  "email": "jane@example.com",
                  "first_name": "Jane",
                  "last_name": "Doe",
                  "attributes": {
                    "plan": "pro",
                    "city": "Berlin"
                  },
                  "status": "unsubscribed",
                  "source": "checkout",
                  "created_at": "2026-03-01T10:00:00Z",
                  "updated_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, has an unknown field, or a field has the wrong type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — status is not one of the allowed values, or a name is over 100 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Contacts"
        ],
        "summary": "Delete contact",
        "description": "Permanently delete a contact and its group memberships.",
        "operationId": "delete_contacts_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Contact ID (con_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/contact-groups": {
      "get": {
        "tags": [
          "Contact Groups"
        ],
        "summary": "List contact groups",
        "description": "List all contact groups on the connected account, newest first, with member counts. Not paginated.",
        "operationId": "get_contact_groups",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "grp_4d8a0c5e-1f7",
                      "organization_id": "org_8d41c7a2-3f6",
                      "name": "Newsletter",
                      "slug": "newsletter",
                      "description": "Opted in at checkout",
                      "member_count": 4200,
                      "created_at": "2026-01-10T08:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Contact Groups"
        ],
        "summary": "Create contact group",
        "description": "Create a contact group. Returns 201 with the group.",
        "operationId": "post_contact_groups",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Group name, 1–80 characters. Names with the same slug (e.g. \"VIP\" and \"vip\") conflict."
                  },
                  "description": {
                    "type": "string",
                    "description": "Optional internal note."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "grp_4d8a0c5e-1f7",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "VIP customers",
                  "slug": "vip-customers",
                  "description": "Accounts over $10k ARR",
                  "member_count": 0,
                  "created_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, or name is missing or over 80 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "name_taken — A group with the same name (slug) already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/contact-groups/{id}": {
      "get": {
        "tags": [
          "Contact Groups"
        ],
        "summary": "Get contact group",
        "description": "Retrieve one contact group with its member count.",
        "operationId": "get_contact_groups_id",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Group ID (grp_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "grp_4d8a0c5e-1f7",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "VIP customers",
                  "slug": "vip-customers",
                  "description": "Accounts over $10k ARR",
                  "member_count": 142,
                  "created_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Contact Groups"
        ],
        "summary": "Update contact group",
        "description": "Update the group’s name and/or description. Omitted fields keep their value; the name cannot be empty. The slug does not change.",
        "operationId": "patch_contact_groups_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Group ID (grp_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New name. Omitting it clears the name."
                  },
                  "description": {
                    "type": "string",
                    "description": "New description. Omitting it clears the description."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "grp_4d8a0c5e-1f7",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "Enterprise customers",
                  "slug": "vip-customers",
                  "description": "Accounts over $10k ARR",
                  "member_count": 142,
                  "created_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, has an unknown field, or a field has the wrong type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Contact Groups"
        ],
        "summary": "Delete contact group",
        "description": "Delete a group and its memberships. The contacts themselves are kept. A group that a broadcast still targets cannot be deleted.",
        "operationId": "delete_contact_groups_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Group ID (grp_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "group_in_use — A broadcast still targets this group. Delete or retarget the broadcast first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/contact-groups/{id}/members": {
      "get": {
        "tags": [
          "Contact Groups"
        ],
        "summary": "List group members",
        "description": "List the contacts in a group, most recently added first. Offset-paginated; total is the group size. An unknown group returns an empty list.",
        "operationId": "get_contact_groups_id_members",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Group ID (grp_...).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 50, max 500 (larger values fall back to 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of rows to skip. Default 0.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "con_0e6b7f21-c58",
                      "organization_id": "org_8d41c7a2-3f6",
                      "email": "jane@example.com",
                      "first_name": "Jane",
                      "last_name": "Doe",
                      "attributes": {
                        "plan": "pro"
                      },
                      "status": "subscribed",
                      "source": "checkout",
                      "created_at": "2026-03-01T10:00:00Z",
                      "updated_at": "2026-03-01T10:00:00Z"
                    }
                  ],
                  "total": 142
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Contact Groups"
        ],
        "summary": "Add group members",
        "description": "Add contacts to a group by ID. Contacts already in the group or not in the account are skipped; added counts new memberships only.",
        "operationId": "post_contact_groups_id_members",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Group ID (grp_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Contact IDs (con_...) to add, at least one."
                  }
                },
                "required": [
                  "contact_ids"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "added": 2
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, or contact_ids is missing or empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Contact Groups"
        ],
        "summary": "Remove group members",
        "description": "Remove contacts from a group. The contacts themselves are kept. IDs that are not members are ignored.",
        "operationId": "delete_contact_groups_id_members",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Group ID (grp_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Contact IDs (con_...) to remove, at least one."
                  }
                },
                "required": [
                  "contact_ids"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, or contact_ids is missing or empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/broadcasts": {
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "List broadcasts",
        "description": "List broadcasts on the connected account, newest first. Cursor-paginated. status is draft, queued, sending or sent.",
        "operationId": "get_broadcasts",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 50, max 200.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The next_cursor value from the previous page. Omit for the first page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "bca_9b3e6d12-a40",
                      "organization_id": "org_8d41c7a2-3f6",
                      "name": "June newsletter",
                      "group_id": "grp_4d8a0c5e-1f7",
                      "from_email": "hello@acme.com",
                      "subject": "June product update",
                      "html": "<h1>June update</h1><p>Here is what is new.</p>",
                      "status": "sent",
                      "total_recipients": 4200,
                      "sent_count": 4188,
                      "failed_count": 12,
                      "created_at": "2026-05-28T14:00:00Z",
                      "started_at": "2026-06-01T09:00:00Z",
                      "completed_at": "2026-06-01T09:42:10Z"
                    }
                  ],
                  "has_more": false,
                  "count": 1,
                  "limit": 50
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — limit is above 200, or cursor is not a value returned as next_cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Create broadcast",
        "description": "Create a draft broadcast. Returns 201 with the broadcast. The from domain is not checked here; if it is not verified, every recipient fails at send time.",
        "operationId": "post_broadcasts",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Internal campaign name, 1–120 characters."
                  },
                  "group_id": {
                    "type": "string",
                    "description": "Contact group to send to (grp_...). Must belong to the connected account."
                  },
                  "from_email": {
                    "type": "string",
                    "description": "Sender, as an address or \"Name <address>\", on a verified domain."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Subject line, 1–500 characters."
                  },
                  "html": {
                    "type": "string",
                    "description": "HTML body. Required unless text or template_id is set. {{email}}, {{first_name}}, {{last_name}} and any contact attribute (e.g. {{plan}}) are filled in per recipient."
                  },
                  "text": {
                    "type": "string",
                    "description": "Plain-text body."
                  },
                  "template_id": {
                    "type": "string",
                    "description": "Saved template (tpl_...) whose html and text are used when the broadcast has no body."
                  }
                },
                "required": [
                  "name",
                  "group_id",
                  "from_email",
                  "subject"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "bca_9b3e6d12-a40",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "June newsletter",
                  "group_id": "grp_4d8a0c5e-1f7",
                  "from_email": "\"Acme\" <hello@acme.com>",
                  "subject": "June product update",
                  "html": "<h1>June update</h1><p>Here is what is new.</p>",
                  "status": "draft",
                  "total_recipients": 0,
                  "sent_count": 0,
                  "failed_count": 0,
                  "created_at": "2026-06-01T12:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, has an unknown field, or a field has the wrong type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — A required field is missing or too long, from_email is invalid, there is no body or template, or group_id is not in this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/broadcasts/{id}": {
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Get broadcast",
        "description": "Retrieve one broadcast with its body and send progress (total_recipients, sent_count, failed_count).",
        "operationId": "get_broadcasts_id",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Broadcast ID (bca_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "bca_9b3e6d12-a40",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "June newsletter",
                  "group_id": "grp_4d8a0c5e-1f7",
                  "from_email": "hello@acme.com",
                  "subject": "June product update",
                  "html": "<h1>June update</h1><p>Here is what is new.</p>",
                  "status": "sending",
                  "total_recipients": 4200,
                  "sent_count": 1630,
                  "failed_count": 4,
                  "created_at": "2026-05-28T14:00:00Z",
                  "started_at": "2026-06-01T09:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Update broadcast",
        "description": "Update a draft broadcast. Send only the fields to change; it returns the updated broadcast.",
        "operationId": "patch_broadcasts_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Broadcast ID (bca_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New internal name, 1–120 characters."
                  },
                  "group_id": {
                    "type": "string",
                    "description": "New target group (grp_...)."
                  },
                  "from_email": {
                    "type": "string",
                    "description": "New sender, as an address or \"Name <address>\"."
                  },
                  "subject": {
                    "type": "string",
                    "description": "New subject line, 1–500 characters."
                  },
                  "template_id": {
                    "type": "string",
                    "description": "New template ID (tpl_...)."
                  },
                  "html": {
                    "type": "string",
                    "description": "New HTML body."
                  },
                  "text": {
                    "type": "string",
                    "description": "New plain-text body."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "bca_9b3e6d12-a40",
                  "organization_id": "org_8d41c7a2-3f6",
                  "name": "June newsletter",
                  "group_id": "grp_4d8a0c5e-1f7",
                  "from_email": "hello@acme.com",
                  "subject": "June product update: highlights",
                  "html": "<h1>June update</h1><p>Here is what is new.</p>",
                  "status": "draft",
                  "total_recipients": 0,
                  "sent_count": 0,
                  "failed_count": 0,
                  "created_at": "2026-05-28T14:00:00Z"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, has an unknown field, or a field has the wrong type.\nnot_editable — The broadcast is no longer a draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — name or subject is empty or too long, or from_email is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Delete broadcast",
        "description": "Delete a draft broadcast. Broadcasts past draft cannot be deleted.",
        "operationId": "delete_broadcasts_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Broadcast ID (bca_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "status": "ok"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — The broadcast does not exist or is no longer a draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/broadcasts/{id}/send": {
      "post": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "Send broadcast",
        "description": "Queue a draft for immediate delivery to the group’s currently subscribed contacts. Takes no body; scheduling is not supported. recipients is the number of contacts queued.",
        "operationId": "post_broadcasts_id_send",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Broadcast ID (bca_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "status": "queued",
                  "recipients": 4200
                }
              }
            }
          },
          "400": {
            "description": "empty_audience — The group has no subscribed contacts. The broadcast stays in draft, so add contacts and send it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "cannot_queue — The broadcast is not a draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/broadcasts/{id}/recipients": {
      "get": {
        "tags": [
          "Broadcasts"
        ],
        "summary": "List broadcast recipients",
        "description": "List per-recipient delivery rows in queue order. Offset-paginated; status is pending, sending, sent or failed, and email_id links to GET /emails/{id}. An unknown broadcast returns an empty list.",
        "operationId": "get_broadcasts_id_recipients",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Broadcast ID (bca_...).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 50, max 500 (larger values fall back to 50).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of rows to skip. Default 0.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "br_7f2c9a41e8d0",
                      "broadcast_id": "bca_9b3e6d12-a40",
                      "contact_id": "con_0e6b7f21-c58",
                      "email": "jane@example.com",
                      "variables": {},
                      "email_id": "em_d2a8f6c1-3b9",
                      "status": "sent",
                      "sent_at": "2026-06-01T09:01:12Z",
                      "created_at": "2026-06-01T09:00:00Z"
                    }
                  ],
                  "total": 4200
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhooks",
        "description": "List all webhooks on the connected account. Secrets are not returned; secret_prefix shows which secret a webhook was created with.",
        "operationId": "get_webhooks",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": "wh_2a7d4e9c-6b1",
                      "organization_id": "org_8d41c7a2-3f6",
                      "url": "https://your-app.com/webhooks/sndr",
                      "events": [
                        "email.delivered",
                        "email.bounced",
                        "email.failed"
                      ],
                      "secret_prefix": "whsec_Xk9pQ2",
                      "active": true,
                      "created_at": "2026-03-10T10:00:00Z"
                    }
                  ],
                  "has_more": false,
                  "count": 1,
                  "limit": 1
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Create webhook",
        "description": "Register a webhook. Returns 201 with the webhook and its signing secret. This is the only time the full secret is shown, so store it now.",
        "operationId": "post_webhooks",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Public HTTPS URL. Hosts that resolve to private, loopback or link-local addresses are rejected."
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Events to receive, at least one of: email.delivered, email.bounced, email.failed, email.unsubscribed (email.queued and email.complained are accepted but not sent)."
                  }
                },
                "required": [
                  "url",
                  "events"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "wh_2a7d4e9c-6b1",
                  "organization_id": "org_8d41c7a2-3f6",
                  "url": "https://your-app.com/webhooks/sndr",
                  "events": [
                    "email.delivered",
                    "email.bounced",
                    "email.failed"
                  ],
                  "secret_prefix": "whsec_Xk9pQ2",
                  "active": true,
                  "created_at": "2026-06-01T12:00:00Z",
                  "secret": "Xk9pQ2vLr8TzW4mN1bYcE7hJ0aSdFgUi"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, url is missing or not a URL, or an event name is not supported.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "plan_limit_reached — The plan’s webhook limit is reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "validation_failed — url is not https, cannot be resolved, or points at a private address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/webhooks/{id}": {
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete webhook",
        "description": "Delete a webhook. No further events are sent to its URL.",
        "operationId": "delete_webhooks_id",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook ID (wh_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Success. No content."
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/webhooks/{id}/signing-keys": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List signing keys",
        "description": "List the webhook’s rotation keys (status primary, secondary or retired) without secrets. A new webhook has no rows until you stage a key; its creation secret signs as primary until then.",
        "operationId": "get_webhooks_id_signing_keys",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook ID (wh_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "webhook_id": "wh_2a7d4e9c-6b1",
                  "webhook_url": "https://your-app.com/webhooks/sndr",
                  "data": [
                    {
                      "id": "whk_61d0a9c7-5e4",
                      "webhook_id": "wh_2a7d4e9c-6b1",
                      "status": "secondary",
                      "created_at": "2026-06-01T13:00:00Z",
                      "created_by": "usr_5b2f8c1e-9a3",
                      "rotation_reason": "scheduled rotation"
                    }
                  ],
                  "count": 1
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/webhooks/{id}/signing-keys/stage": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Stage signing key",
        "description": "Create a secondary key and return its secret once. Events are signed with both keys (v1 and v1s) until you promote or retire it. The body is optional.",
        "operationId": "post_webhooks_id_signing_keys_stage",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook ID (wh_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "Short label stored with the key. Defaults to customer."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional free-form note."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "whk_61d0a9c7-5e4",
                  "webhook_id": "wh_2a7d4e9c-6b1",
                  "status": "secondary",
                  "created_at": "2026-06-01T13:00:00Z",
                  "created_by": "usr_5b2f8c1e-9a3",
                  "rotation_reason": "scheduled rotation",
                  "secret": "Qm3vT8xZ1cR6yH0nB5wL9kD2fJ7sA4gP"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is missing or not valid JSON (send {} for the defaults).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/webhooks/{id}/signing-keys/{key_id}/promote": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Promote signing key",
        "description": "Make a secondary key the primary. The previous primary is retired at once, so update your receiver to the new secret first.",
        "operationId": "post_webhooks_id_signing_keys_key_id_promote",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook ID (wh_...).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "description": "Secondary key ID (whk_...) from the stage call.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "whk_61d0a9c7-5e4",
                  "status": "primary"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "conflict — The key is not a secondary key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/webhooks/{id}/signing-keys/{key_id}/retire": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Retire signing key",
        "description": "Retire a secondary key without promoting it. The primary key cannot be retired; promote a replacement instead.",
        "operationId": "post_webhooks_id_signing_keys_key_id_retire",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook ID (wh_...).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "description": "Secondary key ID (whk_...).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "id": "whk_61d0a9c7-5e4",
                  "status": "retired"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "not_found — Resource does not exist or belongs to a different account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "conflict — The key is not a secondary key (it is the primary or already retired).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/suppressions": {
      "get": {
        "tags": [
          "Suppressions"
        ],
        "summary": "List suppressions",
        "description": "List every suppressed address on the connected account, newest first. Not paginated. reason is bounce, unsubscribe, manual, or the value set when it was added.",
        "operationId": "get_suppressions",
        "security": [
          {
            "OAuth2": [
              "read"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "read",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "organization_id": "org_8d41c7a2-3f6",
                      "address": "hard-bounce@example.com",
                      "reason": "bounce",
                      "detail": "550 5.1.1 user unknown",
                      "created_at": "2026-05-15T10:30:00Z"
                    },
                    {
                      "organization_id": "org_8d41c7a2-3f6",
                      "address": "optout@example.com",
                      "reason": "manual",
                      "created_at": "2026-05-10T08:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the read scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Suppressions"
        ],
        "summary": "Add suppression",
        "description": "Suppress an address, e.g. an opt-out collected outside SNDR. Returns 201; an address that is already suppressed keeps its original reason.",
        "operationId": "post_suppressions",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "address": {
                    "type": "string",
                    "description": "Email address to suppress. Stored lowercase."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Why it is suppressed. Defaults to manual."
                  },
                  "detail": {
                    "type": "string",
                    "description": "Optional note shown in the list."
                  }
                },
                "required": [
                  "address"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Success.",
            "content": {
              "application/json": {
                "example": {
                  "address": "optout@example.com",
                  "reason": "manual"
                }
              }
            }
          },
          "400": {
            "description": "invalid_request — Body is not valid JSON, or address is missing or not an email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/v1/suppressions/{addr}": {
      "delete": {
        "tags": [
          "Suppressions"
        ],
        "summary": "Remove suppression",
        "description": "Remove an address so emails to it are attempted again. Returns 204 even if the address was not suppressed.",
        "operationId": "delete_suppressions_addr",
        "security": [
          {
            "OAuth2": [
              "send"
            ]
          },
          {
            "BearerAuth": []
          }
        ],
        "x-required-scope": "send",
        "parameters": [
          {
            "name": "addr",
            "in": "path",
            "required": true,
            "description": "The email address, e.g. optout@example.com. A percent-encoded @ (%40) works too.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Success. No content."
          },
          "401": {
            "description": "unauthenticated — Missing or invalid Bearer token.\ntoken_expired — Access token expired — exchange the refresh token for a new pair.\ntoken_revoked — Token was explicitly revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "insufficient_scope — Token is missing the send scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/oauth/authorize": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Begin the Authorization Code flow. Redirects the user to the SNDR consent page.",
        "operationId": "oauth_authorize",
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your sndr_... client ID."
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "Success URL. Must exactly match a URI in your app’s redirect allowlist. Receives ?code on approval."
          },
          {
            "name": "failure_redirect_uri",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "Failure URL. Must match a URI in your app’s failure allowlist. Receives ?error on denial. If omitted, the first configured failure URI is used, or redirect_uri with ?error if none is set."
          },
          {
            "name": "scope",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Space-separated scopes: read, send."
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque CSRF token returned verbatim in the callback."
          },
          {
            "name": "code_challenge",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "PKCE: BASE64URL(SHA-256(code_verifier))."
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "S256"
              ]
            },
            "description": "Must be S256 when using PKCE."
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the SNDR consent page, then to your redirect_uri with ?code & ?state (or ?error=access_denied on denial)."
          }
        }
      }
    },
    "/api/cranl/oauth/token": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Exchange an authorization code for tokens, or refresh an access token. Server-side only.",
        "operationId": "oauth_token",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "authorization_code",
                      "refresh_token"
                    ]
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "description": "Required for grant_type=authorization_code."
                  },
                  "redirect_uri": {
                    "type": "string",
                    "description": "Required for grant_type=authorization_code. Must match the authorize request."
                  },
                  "code_verifier": {
                    "type": "string",
                    "description": "Required if code_challenge was used on authorize."
                  },
                  "refresh_token": {
                    "type": "string",
                    "description": "Required for grant_type=refresh_token."
                  }
                },
                "required": [
                  "grant_type",
                  "client_id",
                  "client_secret"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New token pair.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                },
                "example": {
                  "access_token": "sndr_access_token_here",
                  "token_type": "Bearer",
                  "expires_in": 3600,
                  "refresh_token": "sndr_refresh_token_here",
                  "scope": "read send"
                }
              }
            }
          },
          "400": {
            "description": "invalid_grant — code expired/used, PKCE mismatch, or refresh token invalid. unsupported_grant_type. invalid_request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_client — unknown/inactive client_id or wrong client_secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/cranl/oauth/revoke": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Revoke an access or refresh token. Always returns 200 per RFC 7009.",
        "operationId": "oauth_revoke",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  }
                },
                "required": [
                  "token",
                  "client_id",
                  "client_secret"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token revoked (or unknown — always 200)."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "OAuth2": {
        "type": "oauth2",
        "description": "Authorization Code flow with optional PKCE (S256).",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://app.sndr.sh/cranl/authorize",
            "tokenUrl": "https://api.sndr.sh/api/cranl/oauth/token",
            "refreshUrl": "https://api.sndr.sh/api/cranl/oauth/token",
            "scopes": {
              "read": "Read-only access to all GET endpoints.",
              "send": "Send email and create/update/delete every resource."
            }
          }
        }
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Pass the access token: Authorization: Bearer <access_token>."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "invalid_grant"
              },
              "message": {
                "type": "string",
                "example": "Authorization code has already been used"
              },
              "request_id": {
                "type": "string",
                "example": "req_01HXKQ8B3N"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string"
          },
          "token_type": {
            "type": "string",
            "example": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "example": 3600
          },
          "refresh_token": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "example": "read send"
          }
        },
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
        ]
      }
    }
  },
  "externalDocs": {
    "description": "SNDR Partner API documentation",
    "url": "https://sndr.sh/docs/cranl"
  },
  "x-rate-limits": {
    "oauth": "30 requests/minute per IP",
    "resource": "300 requests/minute per IP",
    "note": "Resource quotas (emails/month, domain count, etc.) come from the connected user's SNDR plan."
  }
}
