Webhooks allow you to receive HTTP POST notifications when specific events occur in Memos. Each user can configure their own webhooks to integrate with external services.
Overview
Memos sends webhook notifications for memo-related events:
memo.created - Triggered when a new memo is created
memo.updated - Triggered when a memo is updated
memo.deleted - Triggered when a memo is deleted
Webhooks are dispatched asynchronously and include the full memo object with the event.
Webhook Payload
All webhook requests are sent as HTTP POST with Content-Type: application/json.
Request Structure
{
"url" : "https://your-endpoint.com/webhook" ,
"activityType" : "memos.memo.created" ,
"creator" : "users/123" ,
"memo" : {
"name" : "memos/abc123" ,
"uid" : "abc123" ,
"content" : "This is my memo content" ,
"visibility" : "PRIVATE" ,
"createTime" : "2026-02-28T10:00:00Z" ,
"updateTime" : "2026-02-28T10:00:00Z" ,
"displayTime" : "2026-02-28T10:00:00Z" ,
"creator" : "users/123" ,
"snippet" : "This is my memo..."
}
}
Payload Fields
The target webhook URL (internal field, echoed back)
The event type that triggered the webhook. One of:
memos.memo.created
memos.memo.updated
memos.memo.deleted
Resource name of the user who triggered the event. Format: users/{id}
The full Memo object that triggered the webhook, including all fields like content, visibility, relations, resources, and metadata.
Expected Response
Your webhook endpoint should respond with:
Status code : 200-299 (any 2xx status)
Response body (optional):
{
"code" : 0 ,
"message" : "success"
}
If the response body contains JSON with a non-zero code field, Memos will log it as an error.
Security Features
SSRF Protection
Memos includes built-in protection against Server-Side Request Forgery (SSRF) attacks:
Webhook URLs are validated before registration
Connections to private/reserved IP addresses are blocked
DNS rebinding attacks are prevented
Blocked IP ranges:
127.0.0.0/8 - IPv4 loopback
10.0.0.0/8 - RFC-1918 private networks
172.16.0.0/12 - RFC-1918 private networks
192.168.0.0/16 - RFC-1918 private networks
169.254.0.0/16 - Link-local addresses (includes cloud metadata services)
::1/128 - IPv6 loopback
fc00::/7 - IPv6 unique local addresses
fe80::/10 - IPv6 link-local addresses
Timeout
Webhook requests have a 30-second timeout. If your endpoint doesn’t respond within this time, the request will fail.
Managing Webhooks
List User Webhooks
curl -X GET https://your-memos.com/api/v1/users/123/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response:
{
"webhooks" : [
{
"name" : "users/123/webhooks/webhook-abc123" ,
"url" : "https://your-endpoint.com/webhook" ,
"displayName" : "My Integration" ,
"createTime" : "2026-02-28T10:00:00Z" ,
"updateTime" : "2026-02-28T10:00:00Z"
}
]
}
Create Webhook
curl -X POST https://your-memos.com/api/v1/users/123/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhook": {
"url": "https://your-endpoint.com/webhook",
"displayName": "My Integration"
}
}'
Webhook URLs must use http or https scheme and resolve to a public IP address.
Update Webhook
curl -X PATCH https://your-memos.com/api/v1/users/123/webhooks/webhook-abc123 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webhook": {
"name": "users/123/webhooks/webhook-abc123",
"url": "https://new-endpoint.com/webhook",
"displayName": "Updated Integration"
},
"updateMask": "url,displayName"
}'
Delete Webhook
curl -X DELETE https://your-memos.com/api/v1/users/123/webhooks/webhook-abc123 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Example Implementations
Node.js + Express
const express = require ( 'express' );
const app = express ();
app . post ( '/webhook' , express . json (), ( req , res ) => {
const { activityType , creator , memo } = req . body ;
console . log ( `Received ${ activityType } from ${ creator } ` );
console . log ( `Memo: ${ memo . content } ` );
// Process the webhook
if ( activityType === 'memos.memo.created' ) {
// Handle new memo
}
res . json ({ code: 0 , message: 'success' });
});
app . listen ( 3000 );
Python + Flask
from flask import Flask, request, jsonify
app = Flask( __name__ )
@app.route ( '/webhook' , methods = [ 'POST' ])
def webhook ():
data = request.json
activity_type = data.get( 'activityType' )
creator = data.get( 'creator' )
memo = data.get( 'memo' )
print ( f "Received { activity_type } from { creator } " )
print ( f "Memo: { memo[ 'content' ] } " )
# Process the webhook
if activity_type == 'memos.memo.created' :
# Handle new memo
pass
return jsonify({ "code" : 0 , "message" : "success" })
if __name__ == '__main__' :
app.run( port = 3000 )
package main
import (
" encoding/json "
" log "
" net/http "
)
type WebhookPayload struct {
URL string `json:"url"`
ActivityType string `json:"activityType"`
Creator string `json:"creator"`
Memo interface {} `json:"memo"`
}
func webhookHandler ( w http . ResponseWriter , r * http . Request ) {
var payload WebhookPayload
if err := json . NewDecoder ( r . Body ). Decode ( & payload ); err != nil {
http . Error ( w , err . Error (), http . StatusBadRequest )
return
}
log . Printf ( "Received %s from %s " , payload . ActivityType , payload . Creator )
// Process the webhook
if payload . ActivityType == "memos.memo.created" {
// Handle new memo
}
w . Header (). Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ). Encode ( map [ string ] interface {}{
"code" : 0 ,
"message" : "success" ,
})
}
func main () {
http . HandleFunc ( "/webhook" , webhookHandler )
log . Fatal ( http . ListenAndServe ( ":3000" , nil ))
}
Use Cases
Slack Notifications
Create a webhook that posts new memos to a Slack channel:
const axios = require ( 'axios' );
app . post ( '/webhook' , express . json (), async ( req , res ) => {
const { activityType , memo } = req . body ;
if ( activityType === 'memos.memo.created' ) {
await axios . post ( process . env . SLACK_WEBHOOK_URL , {
text: `New memo: ${ memo . content } ` ,
username: 'Memos Bot'
});
}
res . json ({ code: 0 , message: 'success' });
});
Backup to External Storage
@app.route ( '/webhook' , methods = [ 'POST' ])
def webhook ():
data = request.json
if data[ 'activityType' ] in [ 'memos.memo.created' , 'memos.memo.updated' ]:
memo = data[ 'memo' ]
# Save to S3, Google Drive, etc.
save_to_backup(memo)
return jsonify({ "code" : 0 , "message" : "success" })
Analytics and Monitoring
func webhookHandler ( w http . ResponseWriter , r * http . Request ) {
var payload WebhookPayload
json . NewDecoder ( r . Body ). Decode ( & payload )
// Send to analytics service
analytics . Track ( payload . Creator , payload . ActivityType , map [ string ] interface {}{
"memoId" : payload . Memo [ "uid" ],
})
json . NewEncoder ( w ). Encode ( map [ string ] interface {}{
"code" : 0 ,
"message" : "success" ,
})
}
Troubleshooting
Webhook Not Firing
Verify the webhook is properly registered using the List Webhooks API
Check that the webhook URL is accessible from your Memos server
Ensure the URL uses https (recommended) or http
Verify the URL doesn’t resolve to a private IP address
Timeout Errors
If your endpoint takes longer than 30 seconds to respond:
Return a 200 response immediately
Process the webhook asynchronously in a background job
Use a message queue for heavy processing
Failed Deliveries
Memos does not currently implement automatic retry for failed webhook deliveries. Your endpoint should:
Return a 2xx status code quickly
Handle errors gracefully
Implement your own retry logic if needed
Best Practices
Respond Quickly Always return a 200 response within a few seconds. Process heavy work asynchronously.
Validate Payloads Verify the payload structure matches the expected format before processing.
Use HTTPS Always use HTTPS endpoints in production to protect webhook data in transit.
Log Everything Log incoming webhooks for debugging and auditing purposes.
Limitations
No automatic retry mechanism for failed deliveries
No webhook signature verification (HMAC)
30-second timeout per request
Webhooks are user-specific (not instance-wide)
No filtering by event type (all memo events trigger all webhooks)