evernote-webhooks-events
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>
Allowed Tools
No tools specified
Provided by Plugin
evernote-pack
Claude Code skill pack for Evernote (24 skills)
Installation
This skill is included in the evernote-pack plugin:
/plugin install evernote-pack@claude-code-plugins-plus
Click to copy
Instructions
# 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:
1. Receive webhook notification
2. Use sync API to fetch actual changes
3. Process the retrieved changes
## Instructions
### Step 1: Webhook Endpoint
```javascript
// routes/webhooks.js
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
// Evernote webhook endpoint
router.get('/webhooks/evernote', (req, res) => {
// Evernote sends GET requests for webhooks
const {
userId, // Evernote user ID
guid, // GUID of changed note (if applicable)
notebookGuid,// GUID of changed notebook (if applicable)
reason // Reason for notification
} = req.query;
console.log('Webhook received:', {
userId,
guid,
notebookGuid,
reason,
timestamp: new Date().toISOString()
});
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process asynchronously
processWebhook(userId, guid, notebookGuid, reason)
.catch(err => console.error('Webhook processing error:', err));
});
async function processWebhook(userId, guid, notebookGuid, reason) {
// Queue the sync job
await syncQueue.add('sync-user', {
userId,
guid,
notebookGuid,
reason,
receivedAt: Date.now()
});
}
module.exports = router;
```
### Step 2: Webhook Reasons
```javascript
// Evernote webhook reasons
const WebhookReasons = {
CREATE: 'create', // New note created
UPDATE: 'update', // Note updated
// Note: No DELETE reason - must detect via sync
};
// Handle different webhook types
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:
// Unknown reason - perform full sync check
await performIncrementalSync(userId);
}
}
```
### Step 3: Sync State Management
```javascript
// services/sync-service.js
const Evernote = require('evernote');
class SyncService {
constructor(noteStore) {
this.noteStore = noteStore;
this.lastUpdateCount = 0;
}
/**
* Get current sync state
*/
async getSyncState() {
return this.noteStore.getSyncState();
}
/**
* Check if sync is needed
*/
async needsSync(lastKnownUpdateCount) {
const state = await this.getSyncState();
return state.updateCount > lastKnownUpdateCount;
}
/**
* Perform incremental sync
*/
async incrementalSync(afterUpdateCount) {
const chunks = [];
let currentUpdateCount = afterUpdateCount;
while (true) {
// Get sync chunk
const chunk = await this.noteStore.getFilteredSyncChunk(
currentUpdateCount,
100, // Max entries per chunk
{
includeNotes: true,
includeNotebooks: true,
includeTags: true,
includeExpunged: true // Detect deletions
}
);
chunks.push(chunk);
// Check if more chunks
if (chunk.chunkHighUSN >= chunk.updateCount) {
break;
}
currentUpdateCount = chunk.chunkHighUSN;
}
return this.processChunks(chunks);
}
/**
* Process sync chunks
*/
processChunks(chunks) {
const changes = {
notes: { created: [], updated: [], deleted: [] },
notebooks: { created: [], updated: [], deleted: [] },
tags: { created: [], updated: [], deleted: [] }
};
for (const chunk of chunks) {
// Process notes
if (chunk.notes) {
for (const note of chunk.notes) {
if (note.deleted) {
changes.notes.deleted.push(note.guid);
} else if (note.created === note.updated) {
changes.notes.created.push(note);
} else {
changes.notes.updated.push(note);
}
}
}
// Process expunged (permanently deleted)
if (chunk.expungedNotes) {
changes.notes.deleted.push(...chunk.expungedNotes);
}
// Process notebooks
if (chunk.notebooks) {
for (const notebook of chunk.notebooks) {
changes.notebooks.updated.push(notebook);
}
}
if (chunk.expungedNotebooks) {
changes.notebooks.deleted.push(...chunk.expungedNotebooks);
}
// Process tags
if (chunk.tags) {
for (const tag of chunk.tags) {
changes.tags.updated.push(tag);
}
}
if (chunk.expungedTags) {
changes.tags.deleted.push(...chunk.expungedTags);
}
}
return changes;
}
}
module.exports = SyncService;
```
### Step 4: Webhook Event Processing
```javascript
// services/event-processor.js
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();
}
/**
* Handle incoming webhook (debounced)
*/
async handleWebhook(userId, guid, reason) {
const key = `${userId}`;
// Debounce multiple webhooks for same user
if (this.pendingWebhooks.has(key)) {
clearTimeout(this.pendingWebhooks.get(key));
}
this.pendingWebhooks.set(key, setTimeout(async () => {
this.pendingWebhooks.delete(key);
await this.processUserChanges(userId);
}, this.debounceMs));
}
/**
* Process changes for a user
*/
async processUserChanges(userId) {
// Prevent concurrent processing
if (this.processingLock.has(userId)) {
console.log(`Already processing for user ${userId}`);
return;
}
this.processingLock.set(userId, true);
try {
// Get last known update count
const lastUpdateCount = await this.getStoredUpdateCount(userId);
// Check if sync needed
if (!await this.syncService.needsSync(lastUpdateCount)) {
console.log(`No changes for user ${userId}`);
return;
}
// Perform incremental sync
const changes = await this.syncService.incrementalSync(lastUpdateCount);
// Emit events for changes
this.emitChangeEvents(userId, changes);
// Store new update count
const state = await this.syncService.getSyncState();
await this.storeUpdateCount(userId, state.updateCount);
} finally {
this.processingLock.delete(userId);
}
}
/**
* Emit events for detected changes
*/
emitChangeEvents(userId, changes) {
// Note events
for (const note of changes.notes.created) {
this.emit('note:created', { userId, note });
}
for (const note of changes.notes.updated) {
this.emit('note:updated', { userId, note });
}
for (const guid of changes.notes.deleted) {
this.emit('note:deleted', { userId, guid });
}
// Notebook events
for (const notebook of changes.notebooks.updated) {
this.emit('notebook:updated', { userId, notebook });
}
for (const guid of changes.notebooks.deleted) {
this.emit('notebook:deleted', { userId, guid });
}
// Tag events
for (const tag of changes.tags.updated) {
this.emit('tag:updated', { userId, tag });
}
for (const guid of changes.tags.deleted) {
this.emit('tag:deleted', { userId, guid });
}
// Summary event
this.emit('sync:complete', {
userId,
summary: {
notesCreated: changes.notes.created.length,
notesUpdated: changes.notes.updated.length,
notesDeleted: changes.notes.deleted.length
}
});
}
// Storage methods (implement with your database)
async getStoredUpdateCount(userId) {
// Return stored update count for user
return 0; // Default to 0 for initial sync
}
async storeUpdateCount(userId, updateCount) {
// Store update count for user
}
}
module.exports = EvernoteEventProcessor;
```
### Step 5: Event Handlers
```javascript
// handlers/event-handlers.js
const processor = require('./event-processor');
// Handle new notes
processor.on('note:created', async ({ userId, note }) => {
console.log(`New note created: ${note.title}`);
// Example: Index for search
await searchIndex.indexNote(note);
// Example: Send notification
await notifications.send(userId, {
type: 'note_created',
title: note.title
});
});
// Handle note updates
processor.on('note:updated', async ({ userId, note }) => {
console.log(`Note updated: ${note.title}`);
// Example: Update search index
await searchIndex.updateNote(note);
// Example: Sync to external service
await externalSync.updateNote(userId, note);
});
// Handle note deletions
processor.on('note:deleted', async ({ userId, guid }) => {
console.log(`Note deleted: ${guid}`);
// Example: Remove from search index
await searchIndex.removeNote(guid);
// Example: Clean up related data
await database.cleanupNoteData(guid);
});
// Handle sync completion
processor.on('sync:complete', ({ userId, summary }) => {
console.log(`Sync complete for user ${userId}:`, summary);
// Example: Log metrics
metrics.recordSync({
userId,
...summary
});
});
```
### Step 6: Webhook Registration
```javascript
// Note: Webhook registration is done through Evernote Developer Portal
// API key settings, not through the API itself.
// Webhook configuration in Evernote Developer Portal:
// 1. Go to https://dev.evernote.com/
// 2. Navigate to your API key settings
// 3. Enable webhooks
// 4. Set webhook URL: https://your-domain.com/webhooks/evernote
// For local development, use ngrok:
// ngrok http 3000
// Then update webhook URL temporarily in developer portal
```
### Step 7: Polling Fallback
```javascript
// services/polling-service.js
/**
* Fallback polling for when webhooks aren't available
* or as a backup sync mechanism
*/
class PollingService {
constructor(syncService, options = {}) {
this.syncService = syncService;
this.pollInterval = options.pollInterval || 5 * 60 * 1000; // 5 minutes
this.users = new Map(); // userId -> lastUpdateCount
this.timer = null;
}
/**
* Start polling for a user
*/
addUser(userId, accessToken) {
this.users.set(userId, {
accessToken,
lastUpdateCount: 0
});
}
/**
* Remove user from polling
*/
removeUser(userId) {
this.users.delete(userId);
}
/**
* Start polling loop
*/
start() {
if (this.timer) return;
this.timer = setInterval(async () => {
await this.pollAllUsers();
}, this.pollInterval);
console.log(`Polling started (interval: ${this.pollInterval}ms)`);
}
/**
* Stop polling
*/
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
/**
* Poll all registered users
*/
async pollAllUsers() {
for (const [userId, data] of this.users) {
try {
await this.pollUser(userId, data);
} catch (error) {
console.error(`Poll failed for user ${userId}:`, error.message);
}
}
}
/**
* Poll single user for changes
*/
async pollUser(userId, data) {
const state = await this.syncService.getSyncState();
if (state.updateCount > data.lastUpdateCount) {
console.log(`Changes detected for user ${userId}`);
const changes = await this.syncService.incrementalSync(
data.lastUpdateCount
);
// Update stored count
data.lastUpdateCount = state.updateCount;
// Process changes
await this.processChanges(userId, changes);
}
}
async processChanges(userId, changes) {
// Same processing as webhook handler
}
}
module.exports = PollingService;
```
## 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
- [Webhooks Overview](https://dev.evernote.com/doc/articles/webhooks.php)
- [Synchronization](https://dev.evernote.com/doc/articles/synchronization.php)
- [Developer Portal](https://dev.evernote.com/)
## Next Steps
For performance optimization, see `evernote-performance-tuning`.