| name | evernote-webhooks-events |
| description | Implement Evernote webhook notifications and sync events.
Use when handling note changes, implementing real-time sync,
or processing Evernote notifications.
Trigger with phrases like "evernote webhook", "evernote events",
"evernote sync", "evernote notifications".
|
| allowed-tools | Read, Write, Edit, Bash(curl:*) |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Evernote Webhooks & Events
Overview
Implement Evernote webhook notifications for real-time change detection. Note: Evernote webhooks notify you that changes occurred, but you must use the sync API to retrieve actual changes.
Prerequisites
- Evernote API key with webhook permissions
- HTTPS endpoint accessible from internet
- Understanding of Evernote sync API
How Evernote Webhooks Work
Unlike most APIs, Evernote webhooks only notify that a user's account changed. They do NOT include the actual changes in the payload. You must:
- Receive webhook notification
- Use sync API to fetch actual changes
- Process the retrieved changes
Instructions
Step 1: Webhook Endpoint
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
router.get('/webhooks/evernote', (req, res) => {
const {
userId,
guid,
notebookGuid,
reason
} = req.query;
console.log('Webhook received:', {
userId,
guid,
notebookGuid,
reason,
timestamp: new Date().toISOString()
});
res.status(200).send('OK');
processWebhook(userId, guid, notebookGuid, reason)
.catch(err => console.error('Webhook processing error:', err));
});
async function processWebhook(userId, guid, notebookGuid, reason) {
syncQueue.(, {
userId,
guid,
notebookGuid,
reason,
: .()
});
}
. = router;
Step 2: Webhook Reasons
const WebhookReasons = {
CREATE: 'create',
UPDATE: 'update',
};
async function handleWebhookByReason(userId, guid, reason) {
switch (reason) {
case WebhookReasons.CREATE:
await handleNoteCreated(userId, guid);
break;
case WebhookReasons.UPDATE:
await handleNoteUpdated(userId, guid);
break;
default:
await performIncrementalSync(userId);
}
}
Step 3: Sync State Management
const Evernote = require('evernote');
class SyncService {
constructor(noteStore) {
this.noteStore = noteStore;
this.lastUpdateCount = 0;
}
async getSyncState() {
return this.noteStore.getSyncState();
}
async needsSync(lastKnownUpdateCount) {
const state = await this.getSyncState();
return state.updateCount > lastKnownUpdateCount;
}
async incrementalSync(afterUpdateCount) {
const chunks = [];
let currentUpdateCount = afterUpdateCount;
while (true) {
const chunk = await this.noteStore.getFilteredSyncChunk(
currentUpdateCount,
100,
{
: ,
: ,
: ,
:
}
);
chunks.(chunk);
(chunk. >= chunk.) {
;
}
currentUpdateCount = chunk.;
}
.(chunks);
}
() {
changes = {
: { : [], : [], : [] },
: { : [], : [], : [] },
: { : [], : [], : [] }
};
( chunk chunks) {
(chunk.) {
( note chunk.) {
(note.) {
changes...(note.);
} (note. === note.) {
changes...(note);
} {
changes...(note);
}
}
}
(chunk.) {
changes...(...chunk.);
}
(chunk.) {
( notebook chunk.) {
changes...(notebook);
}
}
(chunk.) {
changes...(...chunk.);
}
(chunk.) {
( tag chunk.) {
changes...(tag);
}
}
(chunk.) {
changes...(...chunk.);
}
}
changes;
}
}
. = ;
Step 4: Webhook Event Processing
const EventEmitter = require('events');
class EvernoteEventProcessor extends EventEmitter {
constructor(syncService, options = {}) {
super();
this.syncService = syncService;
this.processingLock = new Map();
this.debounceMs = options.debounceMs || 5000;
this.pendingWebhooks = new Map();
}
async handleWebhook(userId, guid, reason) {
const key = `${userId}`;
if (this.pendingWebhooks.has(key)) {
clearTimeout(this.pendingWebhooks.get(key));
}
this.pendingWebhooks.set(key, setTimeout(async () => {
this..(key);
.(userId);
}, .));
}
() {
(..(userId)) {
.();
;
}
..(userId, );
{
lastUpdateCount = .(userId);
(! ..(lastUpdateCount)) {
.();
;
}
changes = ..(lastUpdateCount);
.(userId, changes);
state = ..();
.(userId, state.);
} {
..(userId);
}
}
() {
( note changes..) {
.(, { userId, note });
}
( note changes..) {
.(, { userId, note });
}
( guid changes..) {
.(, { userId, guid });
}
( notebook changes..) {
.(, { userId, notebook });
}
( guid changes..) {
.(, { userId, guid });
}
( tag changes..) {
.(, { userId, tag });
}
( guid changes..) {
.(, { userId, guid });
}
.(, {
userId,
: {
: changes...,
: changes...,
: changes...
}
});
}
() {
;
}
() {
}
}
. = ;
Step 5: Event Handlers
const processor = require('./event-processor');
processor.on('note:created', async ({ userId, note }) => {
console.log(`New note created: ${note.title}`);
await searchIndex.indexNote(note);
await notifications.send(userId, {
type: 'note_created',
title: note.title
});
});
processor.on('note:updated', async ({ userId, note }) => {
console.log(`Note updated: ${note.title}`);
await searchIndex.updateNote(note);
await externalSync.updateNote(userId, note);
});
processor.on('note:deleted', async ({ userId, guid }) => {
console.log(`Note deleted: ${guid}`);
await searchIndex.(guid);
database.(guid);
});
processor.(, {
.(, summary);
metrics.({
userId,
...summary
});
});
Step 6: Webhook Registration
Step 7: Polling Fallback
class PollingService {
constructor(syncService, options = {}) {
this.syncService = syncService;
this.pollInterval = options.pollInterval || 5 * 60 * 1000;
this.users = new Map();
this.timer = null;
}
addUser(userId, accessToken) {
this.users.set(userId, {
accessToken,
lastUpdateCount: 0
});
}
removeUser(userId) {
this.users.delete(userId);
}
start() {
if (this.timer) return;
this.timer = setInterval( () => {
.();
}, .);
.();
}
() {
(.) {
(.);
. = ;
}
}
() {
( [userId, data] .) {
{
.(userId, data);
} (error) {
.(, error.);
}
}
}
() {
state = ..();
(state. > data.) {
.();
changes = ..(
data.
);
data. = state.;
.(userId, changes);
}
}
() {
}
}
. = ;
Output
- Webhook endpoint implementation
- Sync state management
- Event-driven change processing
- Event handlers for note lifecycle
- Polling fallback mechanism
Webhook vs Polling
| Aspect | Webhooks | Polling |
|---|
| Latency | Near real-time | Poll interval |
| Rate limit impact | None | Uses API quota |
| Reliability | Depends on network | Consistent |
| Setup complexity | Requires public URL | Simple |
| Recommended | Production | Development/backup |
Error Handling
| Issue | Cause | Solution |
|---|
| Webhook not received | URL not reachable | Verify HTTPS endpoint |
| Duplicate webhooks | Network retries | Implement idempotency |
| Missing changes | Race condition | Re-sync after timeout |
| Sync timeout | Large change set | Increase chunk size |
Resources
Next Steps
For performance optimization, see evernote-performance-tuning.