hail CLI v0.9.0
Changelog
All notable changes to Hail are documented here. The format is based on Keep a Changelog, and Hail adheres to Semantic Versioning.
[0.9.0] β 2026-07-07
Breaking change. Component versions cut alongside this release:
sdk-v0.6.0 (PyPI: hail-sdk==0.6.0), cli-v0.9.0 (Homebrew + GitHub Releases).
POST /callsandPOST /emailsnow require arecipient_consent: bool
field (rejected with 422 if missing orfalse). This attests that the
caller has obtained the lawful consent required to contact the recipient β
Hail does not verify consent itself. Optionalconsent_sourceand
consent_obtained_atfields record how/when consent was obtained; a
required non-emptyconsent_sourceis enforced when the new optional
message_type: "marketing"field is set (default remains
"informational").- Every existing integration must add
recipient_consent: true(or the
real attestation) to its calls/emails, or requests will start failing
with 422 the moment the API deploys this change. The CLI (--recipient-consent),
SDK (recipient_consent=kwarg), and MCP tools (recipient_consent
param) all already require and forward it as of this release β pin to
hail-sdk>=0.6.0andcli-v0.9.0+ before or at the same time as
upgrading against a Hail API instance running this change.
- Every existing integration must add
- Outbound voice calls now open with a fixed, non-configurable AI-disclosure
line spoken before anything else; outbound email carries an equivalent
disclosure appended at send time, outside the caller-authored body. - New global suppression list + one-click email unsubscribe
(List-Unsubscribe/List-Unsubscribe-Postheaders); a pre-send
compliance gate blocks suppressed recipients, premium-rate destinations,
and per-org rate limits before any call or email goes out. - New data-retention purge job and DSAR (lookup/export/delete) tooling for
recipient data requests. /legal/*pages (Terms of Use, AUP, Privacy Policy, DPA, sub-processors,
Cookie Policy) published on hail-website.
[0.8.1] β 2026-07-02
Internal consolidation release. cli-v0.8.1 cut alongside; the SDK is
unchanged and stays at hail-sdk==0.5.0.
- One shared cursor-pagination helper now backs every paginated API route
(was seven hand-copies) and one generic page-walker backs all three CLI
list commands. Wire contract unchanged, with one edge unified: an
empty-stringcursorquery param is now ignored everywhere instead of
returning 400 on some routes. hail email events --allnow emits the same walked-items stderr warning
ascall list --allandemail list --all.
[0.8.0] β 2026-07-02
Component versions cut alongside this umbrella release:
sdk-v0.5.0 (PyPI: hail-sdk==0.5.0), cli-v0.8.0 (Homebrew + GitHub Releases).
GET /emails/{id}/eventsis now cursor-paginated (cursor,limit1..1000,
next_cursorin the response).hail email eventsgains
--cursor/--limit/--all; SDKemails.events()and MCPget_email_events
acceptcursor/limit. Existing callers are unaffected (default limit 100).
[0.7.0] β 2026-07-02
Email deliverability milestone. Every outbound email now has a tracked
lifecycle β delivered, delayed, bounced, complained, rejected, opened,
clicked β visible per message, aggregated per account, and pushed to
customer webhooks.
Component versions cut alongside this umbrella release:
sdk-v0.4.0 (PyPI: hail-sdk==0.4.0), cli-v0.7.0 (Homebrew + GitHub Releases).
Deliverability tracking
- New
email_eventstable (append-only, migration0020); outbound sends write a syntheticsentevent, and SES configuration-set events (Delivery, Bounce, Complaint, Reject, DeliveryDelay, Open, Click) are ingested via SNS β ingest Lambda βPOST /internal/ses-events. Duplicate SNS deliveries are absorbed by a dedup constraint; status transitions are guarded so terminal states never regress.emails.statusgainsdelivered. GET /emails/{id}/eventsβ per-email lifecycle timeline.GET /emails/{id}gainslast_event_at.GET /emails/stats?from=&to=&bucket=hour|dayβ account-level counts, rates (delivery, hard-bounce, complaint, unique open/click), and a zero-filled time series.- Email events join the unified
GET /eventsstream (source: "email",id=email:<uuid>filter). - Webhooks:
email.delivered,email.delivery_delayed,email.opened,email.clickedare new;email.bouncedandemail.complainednow actually fire. - CLI:
hail email events <id>,hail email stats. SDK:client.emails.events(),client.emails.stats(). MCP:get_email_events,get_email_stats. - Infra: SES configuration set (
HAIL_SES_CONFIGURATION_SET, Terraform defaulthail-events), SNS topic with explicit SES publish policy, subscription DLQ. Open/click tracking uses the default SES tracking domain in v1.
[0.6.0] β 2026-06-28
Custom sender domains milestone. Tenants can now send and receive on their own
domain, with DKIM and a custom MAIL FROM configured automatically.
Custom sender domains β send and receive on your own domain
- Register a tenant-controlled domain:
POST /email-domainswithkind=custom(orhail email domain register --kind custom --domain acme.com). Hail calls SESCreateEmailIdentityand auto-configures a custom MAIL FROM onsend.<domain>viaPutEmailIdentityMailFromAttributesβ no AWS-console step for the tenant. - The registration response returns the full DNS record set to publish: three DKIM
_domainkeyCNAMEs plus the MAIL FROMsend.<domain>MX (feedback-smtp.<region>.amazonses.com) and SPF TXT (v=spf1 include:amazonses.com ~all).DnsRecordgains an optionalpriorityfor MX rows. POST /email-domains/{id}/verifyre-polls SES for both DKIM and MAIL FROM, surfacingmail_from_statusalongsideverification_status. On-demand only β clients/console re-poll; there is no background poller.- Receive inbound mail on verified custom domains. Inbound is matched by the owning identity, so a message to two of an org's domains yields one inbound row + webhook per matched domain, with the domain name on the event payload.
- New IAM grant
ses:PutEmailIdentityMailFromAttributesfor the production identity (infra/terraform/iam.tf) β required for the custom MAIL FROM call.
Auth
- The managed console now authenticates to the API with a short-lived Better Auth session JWT (carrying
suband the sessionactiveOrganizationId) rather than a cached per-browser API key. The API resolves the request's organization from the active-org claim, validated against membership β closing a cross-tenant write path.
0.5.0 β 2026-06-12
Inbound email milestone. Hail can now receive mail at hail-mail addresses,
forward it, and fire webhooks on receipt and on bounce/complaint events.
Component versions cut alongside this umbrella release:
sdk-v0.3.0 (PyPI: hail-sdk==0.3.0), cli-v0.5.0 (Homebrew + GitHub Releases).
Inbound email
- SES Receipt Rule β S3 (raw MIME) + Lambda (HMAC-signed JSON) β
POST /internal/ses-eventsingest pipeline. Email.direction='inbound'rows withmessage_id,in_reply_to,references_ids,raw_s3_key, and SES SPF/DKIM/DMARC/spam/virus verdicts persisted.email_attachmentstable β MIME attachments stored as separate S3 objects underattachments/<email_id>/<att_id>.- Threading via
In-Reply-To/Referenceschains. - Spam/virus
FAILverdicts persist the row but suppress forwarding and webhook fan-out. - Idempotency via a partial unique index on
(organization_id, message_id) WHERE direction='inbound'β duplicate SES deliveries short-circuit.
Forwarding
- Per-domain
forward_tolist onemail_domains. Inbound matches trigger one outbound row per target through the existing send loop. - Header-rewrite forwarding (Cloudflare Email Routing-style):
From:=forwarder+<org>@<base_domain>(SPF/DKIM aligned),Reply-To:= original sender,References:preserves threading. - Loop guards: 3-hop max via
X-Hail-Forward-Hops, refuse targets onHAIL_MAIL_BASE_DOMAIN, per-domain rate cap (default 200/hour, override viaforward_rate_per_hour).
Webhooks
- Org-wide
webhook_subscriptions(multi-event firehose) + per-domainemail_domains.webhook_url(single-target ergonomic). Both fire independently. - Background
WebhookWorkerpollswebhook_deliverieswithFOR UPDATE SKIP LOCKED, signs (X-Hail-Signature: t=<unix>,v1=<hmac-sha256>), POSTs via httpx with a private-network guard. - Retry ladder: 0s, 30s, 2m, 10m, 1h, 6h, 24h. After the last slot the row is
dead; 50 consecutive deads auto-disable the subscription. POST /webhooks/{id}/deliveries/{did}/redeliverto replay a dead row.- Webhook secrets are Fernet-encrypted at rest (
HAIL_WEBHOOK_SECRET_KEY), so the worker survives an API restart without secret rotation.
Schema rename: sender_domains β email_domains
- The table now carries both directions, so the historical "sender" name no longer fits. Renames the table, indexes, trigger, CHECK constraints, and the FK column on
emails. - Wire format: routes moved from
/sender-domainsβ/email-domains; audit-action strings fromsender_domain.*βemail_domain.*; pydantic schemasSenderDomain*βEmailDomain*.
API
GET /emails?direction=inbound|outboundfilter.GET /emails/{id}/rawandGET /emails/{id}/attachments/{aid}β 302 redirect to presigned S3 URLs (5-min TTL).PATCH /email-domains/{id}accepts inbound action fields (inbound_enabled,forward_to,webhook_url,forward_rate_per_hour). Settingwebhook_urlmints a secret returned once.POST /email-domains/{id}/rotate-webhook-secretrotates and returns the new plaintext once.- Full
/webhooksCRUD +/rotate-secret+/deliveries+/deliveries/{id}/redeliver.
CLI (cli-v0.5.0)
- New
hail webhookssubcommand tree (create,list,deliveries,redeliver). hail email list --direction inboundfilter.hail email sender-domainrenamed tohail email email-domain(aliased); the legacysd/sender-domainaliases are removed β breaking for users with scripts hitting the old name.- Short-ID prefix resolution on every UUID-taking subcommand (was
call statusonly):email get,email-domain get/verify/delete,webhooks deliveries,webhooks redeliverall accept the 4+ char hex prefix shown inlistoutput, mirroringgit rev-parse's short-hash UX.
SDK (sdk-v0.3.0)
client.webhooks.*resource added.client.emails.list(direction=...)parameter added.client.email_domains.patch()accepts inbound action fields;client.email_domains.rotate_webhook_secret(id)added.SenderDomain*back-compat aliases removed (deprecated sincesdk-v0.2.0) β breaking for users importing the legacy names.
Infrastructure
- Terraform module under
infra/terraform/provisions S3 (raw + attachments + lifecycle), SES Receipt Rule + Rule Set, Lambda + IAM + log group, and emits the MX record + theset-active-receipt-rule-setactivation command. - Stdlib-only
infra/ses-ingest-lambda/handler.pybridges SES events into the HMAC-signed POST. - Three migrations:
0006(rename + auxiliary objects),0007(Email inbound columns +email_attachments+email_domainsaction columns + idempotency partial unique index),0008(webhook_subscriptions+webhook_deliveries). Seedocs/operations.mdβ "Inbound email rollout" for the staged deploy sequence. InboundProviderinterface incore/hailhq/core/providers/email/inbound/withSesInboundProvidershipping andSmtpInboundProviderstubbed for a future cloud-agnostic milestone.- Operator-facing env vars (
HAIL_INBOUND_EMAIL_NAME_PREFIX,HAIL_IAM_USER_NAME) are required in.envβ terragrunt fails fast on missing values instead of silently using a hardcoded default. The inbound bucket name is now a single source of truth (Settings.hail_inbound_bucketis a computed${prefix}-raw), so Terraform and the API can't drift.
Deferred to next milestone
- Custom-domain inbound (tenant MX β SES) β schema is ready; the operator-side flow isn't.
inbound_routestable for per-mailbox routing within custom domains.SmtpInboundProviderimplementation (cloud-agnostic / OSS-only path).- Reply-composition endpoint (
POST /emails/{id}/reply). - MCP push transport for inbound events.
0.1.0 β 2026-04-30
First public release. Outbound phone calls for AI agents, end-to-end.
Phone calls
- Outbound calls via Twilio SIP through LiveKit Cloud.
POST /calls,GET /calls/{id}, andGET /callswith cursor pagination.- Per-org API-key auth with audit logging on every authenticated request.
- Idempotency-Key support on
POST /calls(24h TTL).
Voice pipeline
- Deepgram STT.
- Cartesia TTS (primary) with ElevenLabs fallback.
- Silero VAD (prewarmed once per worker process).
- LiveKit turn-detector for end-of-utterance detection.
- LLM system-prompt mode with OpenAI to Gemini to Anthropic fallback chain.
- LLM BYO-endpoint mode for any OpenAI chat-completions-compatible endpoint.
- Per-turn
call_eventsrows for transcript reconstruction.
Distribution
- OpenAPI 3.1 spec at
openapi/openapi.yamlas the source of truth. hailCLI binary published via GitHub Releases (darwin and linux, amd64 and arm64) and a Homebrew tap.- Remote MCP server bundled with every Hail deploy at
/sse, exposing aplace_calltool. hail-sdkon PyPI (imports ashail) withClient.calls.create / get / list.
Infrastructure
docker compose upbrings upapi,voicebot,mcp, and Postgres.- Alembic migrations for the v1 schema.
- CI runs lint, pytest with a Postgres service container, Go build and test, and a docker-compose build smoke.
Deferred to v1.x
- LiveKit Egress recording (
recording.pyis stubbed;Call.recording_s3_keyis alwaysNULL). idempotency_keysGC sweeper and in-flight reaper.- Inbound calls (
LIVEKIT_SIP_INBOUND_TRUNK_IDis reserved in env but unused). - SMS channel (Twilio outbound and inbound).
- Email channel (AWS SES outbound and inbound).
CallEventdedupe across voicebot redispatch.