This feature is only available on the Royal Jelly plan
Webhook Formats
Jelly supports two webhook payload formats:
Structured Format (Recommended)
New webhooks use the structured format by default. This format provides:
- Event subscriptions: Choose which events trigger webhooks
- HMAC payload signing: Verify webhook authenticity
- Consistent JSON structure: Predictable payload format across all events
- Additional event types: Beyond just new messages
Legacy Format
Existing webhooks created before the structured format was introduced use the legacy format. Legacy webhooks:
- Only receive
new_messageevents - Use a flat JSON structure (documented below for backwards compatibility)
- Do not support HMAC signing
You can upgrade a legacy webhook to the structured format from the Integrations settings page.
Configuration
Webhooks can be configured at the team level in Settings โ Integrations with the following options:
- URL: The HTTPS endpoint to receive the webhook payload (required)
- Basic Authentication (optional):
- Username
- Password
- Event Subscriptions (structured format only): Select which events trigger webhooks
- Signing Secret (structured format only): Auto-generated secret for HMAC verification
Structured Payload Format
All structured webhook payloads follow this envelope format:
{
"event": "event_type",
"created_at": "2024-01-15T10:30:00Z",
"data": {
// Event-specific data
}
}
Event Types
new_message
Triggered when a new message is sent or received.
{
"event": "new_message",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"message": {
"id": "abc123",
"subject": "Re: Your inquiry",
"sent_at": "2024-01-15T10:30:00Z",
"from": ["sender@example.com"],
"to": ["recipient@example.com"],
"cc": [],
"text_body": "Plain text content",
"html_body": "<p>HTML content</p>",
"inbound": true
},
"conversation": {
"id": "conversation-slug",
"subject": "Your inquiry",
"status": "open",
"labels": [
{"id": "label-id", "name": "Support", "color": "#ff0000"}
]
},
"is_new_conversation": false
}
}
assigned
Triggered when a conversation is assigned to a team member.
{
"event": "assigned",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"assignee": {
"id": "member-id",
"name": "John Doe",
"email": "john@example.com"
},
"conversation": {
"id": "conversation-slug",
"subject": "Customer question",
"status": "open",
"labels": []
},
"assigner": {
"id": "assigner-id",
"name": "Jane Smith",
"email": "jane@example.com"
}
}
}
comment_added
Triggered when a team member adds an internal comment/note.
{
"event": "comment_added",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"comment": {
"id": "note-id",
"body": "This customer needs priority support",
"created_at": "2024-01-15T10:30:00Z",
"author": {
"id": "member-id",
"name": "John Doe",
"email": "john@example.com"
}
},
"conversation": {
"id": "conversation-slug",
"subject": "Customer question",
"status": "open",
"labels": []
}
}
}
conversation_archived
Triggered when a conversation is archived/closed.
{
"event": "conversation_archived",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"conversation": {
"id": "conversation-slug",
"subject": "Resolved issue",
"status": "archived",
"labels": []
},
"actor": {
"id": "member-id",
"name": "John Doe",
"email": "john@example.com"
}
}
}
conversation_unarchived
Triggered when a conversation is reopened/unarchived.
{
"event": "conversation_unarchived",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"conversation": {
"id": "conversation-slug",
"subject": "Follow-up needed",
"status": "open",
"labels": []
},
"actor": {
"id": "member-id",
"name": "John Doe",
"email": "john@example.com"
}
}
}
Request Details
HTTP Method
POST
Content Type
application/json
Headers
All webhook requests include these headers:
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Jelly-Event |
The event type (e.g., new_message) |
X-Jelly-Timestamp |
Unix timestamp of when the request was sent |
X-Jelly-Signature |
HMAC-SHA256 signature (structured format only) |
Authentication
If basic authentication is configured, the webhook request will include an Authorization header:
Authorization: Basic [base64-encoded-credentials]
Verifying Webhook Signatures
For structured webhooks, you should verify the X-Jelly-Signature header to ensure the payload is authentic.
Verification Steps
- Get the raw request body (before JSON parsing)
- Get your signing secret from the Integrations settings page
- Compute the expected signature:
sha256=+ HMAC-SHA256(signing_secret, raw_body) - Compare with the
X-Jelly-Signatureheader
Example (Ruby)
def verify_webhook(request, signing_secret)
signature = request.headers['X-Jelly-Signature']
payload = request.raw_post
expected = "sha256=" + OpenSSL::HMAC.hexdigest('SHA256', signing_secret, payload)
ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end
Example (Node.js)
const crypto = require('crypto');
function verifyWebhook(payload, signature, signingSecret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', signingSecret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Error Handling and Retries
- Failed Deliveries: If a webhook delivery fails (non-2xx response or timeout), the error is logged and visible in the Integrations settings
- Automatic Deactivation: Webhooks are automatically deactivated after 10 consecutive failures over 24 hours
- Retries: Failed webhook deliveries are not automatically retried
- Reactivation: Deactivated webhooks can be reactivated from the Integrations settings page
- Success Clearing: Successful deliveries automatically clear any previous error state
Security Considerations
It’s important to remember that using webhooks will send your message data outside of Jelly, so you are responsible for the security of that data.
- Use HTTPS URLs: Webhook URLs should use HTTPS for secure transmission
- Verify Signatures: Always verify the
X-Jelly-Signatureheader for structured webhooks - Use Authentication: Configure basic authentication or verify signatures for endpoint security
- Disable When Not Needed: If you no longer need your webhook, disable it promptly
Testing Webhooks
To test your webhook integration:
- Configure a webhook URL using a service like webhook.site or ngrok
- Send a test email to your Jelly inbox
- Verify the payload structure and signature
- Check the webhook status in your team’s Integrations settings for any errors
Integration Tips
- Your integration should gracefully ignore any keys in the JSON response that it does not recognise
- Do not assume fields are always present or always of a certain length
- Email can have multiple senders, despite that seeming unusual
- The subject of a message and conversation may differ (participants can change subjects)
Legacy webhook Payload Format
When a new message is created in a conversation, Jelly sends the following JSON payload:
{
"url": "https://app.letsjelly.com/[team-id]/conversations/[conversation-id]#message_[message-id]",
"subject": "Message subject line",
"sent_at": "2024-01-15T10:30:00Z",
"from": ["sender@example.com", "Another Sender Name <another-sender@example.com>"],
"to": ["recipient@example.com", "Another Recipient <recipient2@example.com>"],
"cc": ["cc@example.com"],
"bcc": ["bcc@example.com"],
"html_body": "<html>Message HTML content</html>",
"text_body": "Plain text version of the message",
"attachments_count": 2,
"new_conversation": true,
"conversation": {
"id": "conversation-id",
"url": "https://app.letsjelly.com/[team-id]/conversations/[conversation-id]",
"subject": "Conversation subject",
"messages_count": 5,
"got_by": "[{\"name\":\"John Doe\",\"email\":\"john@team.com\"}]"
}
}
Field Descriptions
Message Fields
url(string): Direct link to the specific message within the conversationsubject(string): Subject line of the messagesent_at(datetime): ISO 8601 timestamp when the message was sentfrom(array): List of sender email addresses, may include display namesto(array): List of recipient email addresses, may include display namescc(array): List of CC recipient email addresses, may include display namesbcc(array): List of BCC recipient email addresses, may include display names. This is only ever present on outgoing messages, as recipients are never given the contents of Bcc fields.html_body(string): HTML content of the message body. This may be empty or null if the message only had a text parttext_body(string): Plain text version of the message body. This may be empty or null if the message only had an HTML partattachments_count(integer): Number of attachments on the messagenew_conversation(boolean):trueif this is the first message in a new conversation,falsefor replies or follow-up messages
Conversation Fields
conversation.id(string): Unique identifier for the conversation – typically a parameterized version of the conversation subject. This corresponds with the URL on the website.conversation.url(string): Link to view the full conversationconversation.subject(string): Subject line of the conversationconversation.custom_subject(string, optional): Customised subject of conversation. This may not be present.conversation.messages_count(integer): Total number of messages in the conversationconversation.got_by(string): JSON-encoded array of assigned users with their name and email
Legacy Webhook Triggers
Legacy webhooks are triggered when:
- A new inbound email is received by Jelly
- A team member sends an outbound reply
- A team member starts a new conversation from Jelly
Legacy webhooks are NOT currently triggered for:
- System notifications
- Internal notes or comments
- Assignments
- Message status changes (archive, label, etc.)
Request Details
HTTP Method
POST
Content Type
application/json
Authentication
If basic authentication is configured, the webhook request will include an Authorization header:
Authorization: Basic [base64-encoded-credentials]
Error Handling
Failed Deliveries: If a webhook delivery fails (non-2xx response or timeout), the error is logged with:
- Error message
- Timestamp of the failure
- These can be viewed in the webhook integration settings
Retries: Failed webhook deliveries are not automatically retried
Error Clearing: Successful deliveries automatically clear any previous error state
Integration tips
Your integration should gracefully ignore any keys in the JSON response that it does not recognise, so that future changes to the webhook response don’t cause any issues.
You should not build in any assumptions that fields are always present, or always of a certain length. Email is pretty weird, so – for example, an email can be from multiple addresses, despite that seeming nonsensical given the way email is typically used.
The subject of a message and the subject of a conversation may not always be the same - the subject of a conversation is typically set by the first message, but participants in email are free to change subjects whenever they like.
If someone customises a conversation subject in Jelly, we will also include that value, but otherwise it will be absent.