Skip to main content

Troubleshooting

Start from the HTTP status and error_code. The JSON body has no translated sentence. Field problems are in field_errors.

GET /api/v1/health only reports that the process is up. It does not check Postgres, Redis, or the lane workers.

The process will not start​

DEBUG=false raises ImproperlyConfigured and exits when any of these are missing:

  • SECRET_KEY
  • IDENTITY_ISSUER or IDENTITY_AUDIENCE
  • IDENTITY_JWKS or IDENTITY_JWKS_FILE (a URL in IDENTITY_JWKS_URL is not enough)
  • POSTGRES_DATABASE_URL
  • REDIS_URL
  • EMAIL_CREDENTIALS_KEY, EMAIL_VARIABLES_KEY, or EMAIL_HASH_PEPPER

JWT_HS256_FALLBACK_SECRET with DEBUG=false also refuses to start, unless ALLOW_JWT_HS256_FALLBACK is set. Leave the HS256 secret unset in production.

Postgres SSL errors against an internal database: POSTGRES_SSL_REQUIRE defaults to true when DEBUG=false. Set POSTGRES_SSL_REQUIRE=false for a database that does not speak TLS.

The caller cannot reach email-service​

What you seeWhat to change
storage or hosting logs a private-address refusalSet EMAIL_SERVICE_ALLOW_PRIVATE=true on that caller. localhost and host.docker.internal are private. identity-service has no such setting
401 unauthorizedThe esk_ key is missing, revoked, or not the value printed once by create_service_key
403 lane_not_allowedThe key's --lanes omit this lane. Identity needs auth,transactional
403 forbiddenThe template key does not start with one of the key's prefixes
403 company_mismatchThe key's allowed_company_ids omit this company, or the JWT company does not match

The call succeeded and nothing was sent​

202 with skipped_reason is finished. Do not retry it.

skipped_reasonMeaning
no_ruleNo enabled email rule for that company and event. default_enabled on the catalog does not send mail. Auth-lane events create a built-in rule on the first rules list or the first event. identity.auth.magic_link.staff_blocked has no rule: use /send
no_recipientsThe rule is enabled and has no address

A message stuck in queued means the lane worker is not running. The image starts one worker per lane. A checkout does not. Broadcasts need --lane bulk. See Scheduled jobs.

library_not_synced (503) means migrations have not synced the built-in designs. Run manage.py migrate.

renderer_unavailable (503) means Node or renderer/compose.mjs failed or hit EMAIL_COMPOSE_TIMEOUT_SECONDS. Run npm ci in the service root.

The provider refused the message​

error_codeMeaning
provider_not_configuredThis send has no usable credentials
platform_sender_not_allowedNon-auth mail, the company has no provider, and the company is not in EMAIL_PLATFORM_COMPANY_IDS. Auth mail still uses the platform fallback. The same code is HTTP 403 when a company sets from_email to the platform From address
company_smtp_disabledCompany SMTP is off (EMAIL_ALLOW_COMPANY_SMTP defaults to false)
provider_host_not_publicThe company SMTP host is missing, private, or not a public address
provider_tls_unverifiedThe SMTP relay certificate or hostname failed TLS verification. Fix the relay certificate. For a private platform relay only, EMAIL_SMTP_VERIFY_CERT=false
provider_not_availableThe name is not resend, ses, or smtp. Mailjet is not registered
lane_pausedStaff paused the lane, or this company's provider returned 401 or 403. Other companies keep sending. Resume with POST /api/v1/lanes/{lane}/resume
provider_unauthorized on a broadcastThe Resend key is Sending access. Broadcasts need Full access. See Providers
provider_test_failedThe test send was refused. Read the provider response in the admin, not a sentence in this JSON

Resend webhook 401: the signing secret does not match. The platform endpoint uses RESEND_WEBHOOK_SECRET. ?company_id= uses that company's secret and does not fall back to the platform secret.

Statuses that stay at "handed to the provider": the Resend webhook is missing email.sent, email.delivered, or the failure events. Unsubscribes from a Resend broadcast page need contact.updated.

Auth mail was rejected​

error_codeMeaning
auth_single_recipientMore than one address on an auth send, batch item, or event
auth_event_rule_forbiddenA company rule on an auth-lane event
rule_built_inDelete, disable, or extra recipients on a built-in auth rule
auth_link_missingThe copy dropped magic_link_url or another required link variable
auth_link_host_not_allowedThe link host is not on EMAIL_AUTH_LINK_HOSTS. DEBUG=false drops localhost, 127.0.0.1, and ::1
auth_link_misplacedA link variable sits in an image src, a style, or an alt
auth_literal_linkA literal URL in the subject, preheader, or document text
variable_url_not_allowedA URL variable is not https, mailto, or tel, or the host is not allowlisted
recipient_suppressedAuth lane, hard bounce. Do not retry that address. HTTP 422
recipient_rate_limited5 auth messages per recipient per company per 10 minutes
company_rate_limitedAuth: EMAIL_COMPANY_AUTH_LIMIT (default 30) per EMAIL_COMPANY_AUTH_WINDOW_SECONDS (default 60). Transactional: 1000 per company per hour

An auth message that becomes expired missed its TTL before the provider accepted it. The variables are deleted. shellui_email_auth_ttl_expiries counts those rows.

Broadcasts and newsletters​

Every sign-up hits the per-IP limit (HTTP 429). All visitors share one address, usually the proxy. Check that the proxy sets EMAIL_CLIENT_IP_HEADER (default X-Forwarded-For) and that EMAIL_CLIENT_IP_TRUSTED_PROXIES matches the number of proxies that append to it (default 1 for Coolify). A missing header or a value that is not an IP falls back to REMOTE_ADDR. Visitors in the same IPv6 /64 share one limit. If the service has no proxy in front, set EMAIL_CLIENT_IP_HEADER= (empty).

audience_too_large: the audience is over EMAIL_BROADCAST_MAX_RECIPIENTS (default 10000).

newsletter_unavailable: the company cannot send the confirmation email because it has no provider.

turnstile_failed: the token is missing or rejected. turnstile_unavailable: Cloudflare did not answer.

origin_not_allowed: the browser Origin is not in the list's allowed_origins.

subscriber_unsubscribed: a consented import tried to add an address that already unsubscribed.

Public sign-up returns 202 for a new address, a pending address, and an address that is already confirmed. That is intentional. The form cannot tell those cases apart.