Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions fern/apis/api/definition/events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ types:
enum:
- name: MESSAGE_RECEIVED
value: message.received
- name: MESSAGE_RECEIVED_ENVELOPE
value: message.received.envelope
- name: MESSAGE_RECEIVED_SPAM
value: message.received.spam
- name: MESSAGE_RECEIVED_BLOCKED
Expand All @@ -37,6 +39,7 @@ types:
docs: Event types for which to send events.

MessageReceivedEventType:
docs: Full-content received variants only. The metadata-only envelope has a separate event schema.
enum:
- name: MESSAGE_RECEIVED
value: message.received
Expand Down Expand Up @@ -141,6 +144,53 @@ types:
message_id: messages.MessageId
timestamp: Timestamp

AuthenticationVerdict:
enum:
- pass
- fail
- none
- temperror
- permerror
- neutral
- softfail

MessageAuthenticationResults:
docs: Optional producer-verified verdicts. Absence does not imply authentication passed; raw Authentication-Results headers are not trusted evidence.
properties:
spf: optional<AuthenticationVerdict>
dkim: optional<AuthenticationVerdict>
dmarc: optional<AuthenticationVerdict>

ReceivedEnvelopeMessage:
docs: Selected metadata only. Labels and Message-ID remain user-controlled metadata; invalid addresses are omitted. No subject, body, preview, attachment or content URL is included.
properties:
organization_id: string
pod_id: string
inbox_id: inboxes.InboxId
thread_id: threads.ThreadId
message_id: messages.MessageId
timestamp: Timestamp
created_at: Timestamp
updated_at: Timestamp
from: optional<string>
to: optional<list<string>>
cc: optional<list<string>>
bcc: optional<list<string>>
reply_to: optional<list<string>>
labels: list<string>
authentication_results: optional<MessageAuthenticationResults>
headers:
type: map<string, string>
docs: Currently empty. No raw header values are allowlisted.

MessageReceivedEnvelopeEvent:
docs: Opt-in pilot webhook, separate from the full-content received event. Requires support activation. Not emitted on WebSocket connections.
properties:
type: literal<"event">
event_type: literal<"message.received.envelope">
event_id: EventId
message: ReceivedEnvelopeMessage

MessageReceivedEvent:
docs: A message was received. Spam, blocked, and unauthenticated received-message events use the same payload shape with different `event_type` values.
properties:
Expand Down
9 changes: 9 additions & 0 deletions fern/apis/api/definition/webhooks/events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ types:
docs: Signature of webhook message.

webhooks:
messageReceivedEnvelope:
display-name: Message Received Envelope (Pilot)
method: POST
headers:
svix-id: SvixId
svix-signature: SvixSignature
svix-timestamp: SvixTimestamp
payload: events.MessageReceivedEnvelopeEvent

messageReceived:
display-name: Message Received
method: POST
Expand Down
13 changes: 13 additions & 0 deletions fern/pages/core-concepts/permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ Threads are groupings of messages and have no permissions of their own. Every th
| `webhook_update` | Update webhooks |
| `webhook_delete` | Delete webhooks |

For organizations enrolled in the support-controlled content-boundary pilot, an enabled
console content lock adds `message_read` checks when creating or retaining a body-bearing
received webhook. This includes channel and HTTP-header updates and idempotent returns
of existing subscriptions. Restricted-label requirements still apply. The
`message.received.envelope` webhook contains selected metadata and does not require
`message_read`. Organizations outside the enabled pilot retain their existing received
webhook permission behavior. Existing security-event requirements are unchanged.

The lock applies to console JWTs, not independently issued API keys or internal proxy
credentials. Prior body subscriptions, content credentials and signed content URLs need
separate cleanup before activation. Locked pilot organizations use a signed REST client
and envelope webhooks; the console mail UI and WebSocket content stream are unavailable.

### Domains

| Permission | Description |
Expand Down
20 changes: 20 additions & 0 deletions fern/pages/webhooks/webhooks-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ All webhook payloads follow the same basic structure:
}
```

## Metadata-only received envelope (opt-in pilot)

`message.received.envelope` is available only after support enables the content-boundary
pilot for your organization. It has its own `MessageReceivedEnvelopeEvent` schema;
do not parse it as `MessageReceivedEvent`, which requires a full message and thread.
It includes IDs, timestamps, bare addresses, labels, optional verified authentication
results and an empty `headers` object. It excludes subject, body, preview, attachments,
content URLs and the thread object. Malformed address values are omitted. Labels and
Message-ID remain user-controlled strings; selected metadata is not a general trust signal.

Authentication results can be absent on both new and legacy rows until the separate
verification producer is deployed. Absence must never be interpreted as a passing verdict.
Raw Authentication-Results headers are not forwarded as trusted results.

The event has a stable ID distinct from the corresponding full-content event. Handle
redelivery and deduplicate by `svix-id`; ordering is not guaranteed. This pilot does not
add envelope events to WebSockets. Schema definitions do not imply that an already
installed SDK release supports the new event; handle raw JSON until using a compatible
release. Ordinary full-content event examples below are unchanged.

## Parsing events with SDKs

The AgentMail SDKs export typed classes for each webhook payload shape, so you can parse raw payloads into fully typed objects.
Expand Down
Loading