Skip to content

Commit c10c17b

Browse files
authored
Merge branch 'main' into feat/goalProgressNotifications
2 parents 1723510 + 3cc0496 commit c10c17b

78 files changed

Lines changed: 7207 additions & 150 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DISASTER_RECOVERY_RUNBOOK.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,6 @@ curl -X POST https://api.nestera.io/api/backup/restore-test \
139139
| On-call engineer | ops@nestera.io |
140140
| AWS account owner | devops@nestera.io |
141141
| Escalation | dev@nestera.io |
142+
143+
144+
// Working on task

WEBHOOK_INTEGRATION.md

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# Webhook Integration Guide
2+
3+
Nestera supports outbound webhooks so your service can receive real-time notifications when platform events occur (deposits, withdrawals, goal completions, and more).
4+
5+
---
6+
7+
## Overview
8+
9+
| Feature | Detail |
10+
|---|---|
11+
| Protocol | HTTPS POST |
12+
| Payload format | JSON |
13+
| Signing | HMAC-SHA256 (`X-Nestera-Signature`) |
14+
| Retry strategy | Exponential back-off — 1 min, 5 min, 30 min, 2 hrs (5 attempts max) |
15+
| Delivery timeout | 10 seconds |
16+
17+
---
18+
19+
## Event Schema
20+
21+
Every webhook delivery has this envelope:
22+
23+
```json
24+
{
25+
"event": "savings.deposit",
26+
"data": {
27+
"userId": "abc-123",
28+
"amount": "50.00",
29+
"currency": "USDC",
30+
"transactionHash": "0xabc...",
31+
"timestamp": "2026-06-02T03:00:00.000Z"
32+
}
33+
}
34+
```
35+
36+
### Available Events
37+
38+
| Event | Description |
39+
|---|---|
40+
| `savings.deposit` | A deposit was made to a savings account |
41+
| `savings.withdrawal` | A withdrawal was processed |
42+
| `savings.goal_completed` | A savings goal reached its target |
43+
| `savings.goal_created` | A new savings goal was created |
44+
| `savings.interest_accrued` | Interest was added to an account |
45+
| `user.kyc_approved` | KYC verification approved |
46+
| `user.kyc_rejected` | KYC verification rejected |
47+
| `webhook.test` | Sent when you trigger a test delivery |
48+
49+
Use `*` to subscribe to all events, or `savings.*` to subscribe to all savings events.
50+
51+
---
52+
53+
## Authentication
54+
55+
All webhook management endpoints require a Bearer token:
56+
57+
```http
58+
Authorization: Bearer <your-jwt-token>
59+
```
60+
61+
---
62+
63+
## Registering a Webhook
64+
65+
```http
66+
POST /webhooks
67+
Content-Type: application/json
68+
Authorization: Bearer <token>
69+
70+
{
71+
"url": "https://your-service.com/webhooks",
72+
"events": ["savings.deposit", "savings.withdrawal"],
73+
"description": "Production deposit notifications"
74+
}
75+
```
76+
77+
**Response:**
78+
79+
```json
80+
{
81+
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
82+
"url": "https://your-service.com/webhooks",
83+
"events": ["savings.deposit", "savings.withdrawal"],
84+
"status": "ACTIVE",
85+
"secret": "a8f3b2c1...",
86+
"description": "Production deposit notifications",
87+
"createdAt": "2026-06-02T03:00:00.000Z"
88+
}
89+
```
90+
91+
> **Important:** Save the `secret` — it is shown only once and is used to verify incoming requests.
92+
93+
---
94+
95+
## Verifying Webhook Signatures
96+
97+
Every delivery includes these headers:
98+
99+
| Header | Value |
100+
|---|---|
101+
| `X-Nestera-Signature` | `sha256=<hmac-hex>` |
102+
| `X-Nestera-Timestamp` | Unix timestamp in milliseconds |
103+
| `X-Nestera-Event` | Event name, e.g. `savings.deposit` |
104+
105+
To verify, compute `HMAC-SHA256(rawBody, secret)` and compare with the signature:
106+
107+
### Node.js
108+
109+
```js
110+
const crypto = require('crypto');
111+
112+
function verifySignature(rawBody, secret, signatureHeader) {
113+
const expected = 'sha256=' + crypto
114+
.createHmac('sha256', secret)
115+
.update(rawBody)
116+
.digest('hex');
117+
118+
return crypto.timingSafeEqual(
119+
Buffer.from(signatureHeader),
120+
Buffer.from(expected),
121+
);
122+
}
123+
```
124+
125+
### Python
126+
127+
```python
128+
import hmac, hashlib
129+
130+
def verify_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
131+
expected = 'sha256=' + hmac.new(
132+
secret.encode(), raw_body, hashlib.sha256
133+
).hexdigest()
134+
return hmac.compare_digest(signature_header, expected)
135+
```
136+
137+
> Always use the **raw request body** (before JSON parsing) when computing the HMAC.
138+
139+
---
140+
141+
## API Reference
142+
143+
### List webhooks
144+
```
145+
GET /webhooks
146+
```
147+
148+
### Get a webhook
149+
```
150+
GET /webhooks/:id
151+
```
152+
153+
### Update a webhook
154+
```
155+
PATCH /webhooks/:id
156+
```
157+
Body fields: `url`, `events`, `secret`, `description` (all optional).
158+
159+
### Delete a webhook
160+
```
161+
DELETE /webhooks/:id
162+
```
163+
164+
### Disable a webhook
165+
```
166+
PATCH /webhooks/:id/disable
167+
```
168+
169+
### Enable a webhook
170+
```
171+
PATCH /webhooks/:id/enable
172+
```
173+
174+
### Get delivery logs
175+
```
176+
GET /webhooks/:id/deliveries
177+
```
178+
Returns up to 100 most recent delivery attempts in descending order.
179+
180+
### Send a test event
181+
```
182+
POST /webhooks/:id/test
183+
```
184+
Sends a `webhook.test` event to the configured URL and returns the delivery result immediately.
185+
186+
---
187+
188+
## Retry Logic
189+
190+
Failed deliveries (non-2xx response or connection error) are automatically retried with exponential back-off:
191+
192+
| Attempt | Delay after failure |
193+
|---|---|
194+
| 2nd | 1 minute |
195+
| 3rd | 5 minutes |
196+
| 4th | 30 minutes |
197+
| 5th | 2 hours |
198+
199+
After 5 failed attempts the delivery is marked `FAILED` and no further retries are scheduled.
200+
201+
---
202+
203+
## Delivery Monitoring
204+
205+
Check the delivery log to see the status of recent events:
206+
207+
```http
208+
GET /webhooks/:id/deliveries
209+
```
210+
211+
Each delivery record includes:
212+
213+
```json
214+
{
215+
"id": "...",
216+
"eventName": "savings.deposit",
217+
"status": "FAILED",
218+
"attempts": 3,
219+
"responseStatus": 503,
220+
"responseBody": "Service Unavailable",
221+
"errorMessage": null,
222+
"nextRetryAt": "2026-06-02T03:30:00.000Z",
223+
"createdAt": "2026-06-02T03:00:00.000Z"
224+
}
225+
```
226+
227+
### Delivery Statuses
228+
229+
| Status | Meaning |
230+
|---|---|
231+
| `PENDING` | Awaiting delivery or scheduled for retry |
232+
| `SUCCESS` | Delivered with a 2xx HTTP response |
233+
| `FAILED` | All retry attempts exhausted |
234+
235+
---
236+
237+
## Security Best Practices
238+
239+
1. **Always verify the signature** before processing any webhook payload.
240+
2. **Respond quickly** — return a 2xx within 10 seconds. Do heavy work asynchronously.
241+
3. **Use HTTPS** for your webhook endpoint.
242+
4. **Rotate secrets periodically** using the `PATCH /webhooks/:id` endpoint.
243+
5. **Deduplicate** using the delivery `id` if your endpoint may receive duplicates during retries.
244+
245+
---
246+
247+
## Example: Handling a Deposit Webhook
248+
249+
```ts
250+
import { createHmac, timingSafeEqual } from 'crypto';
251+
import express from 'express';
252+
253+
const app = express();
254+
app.use(express.raw({ type: 'application/json' }));
255+
256+
app.post('/webhooks', (req, res) => {
257+
const sig = req.headers['x-nestera-signature'] as string;
258+
const secret = process.env.WEBHOOK_SECRET!;
259+
260+
const expected = 'sha256=' + createHmac('sha256', secret)
261+
.update(req.body)
262+
.digest('hex');
263+
264+
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
265+
return res.status(401).send('Invalid signature');
266+
}
267+
268+
const { event, data } = JSON.parse(req.body.toString());
269+
console.log(`Received ${event}:`, data);
270+
271+
res.json({ received: true });
272+
});
273+
```

backend/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"test:watch": "jest --watch",
1919
"test:cov": "jest --coverage",
2020
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21-
"test:e2e": "jest --config ./test/jest-e2e.js"
21+
"test:e2e": "jest --config ./test/jest-e2e.js",
22+
"generate:openapi": "ts-node -r tsconfig-paths/register scripts/generate-openapi-spec.ts"
2223
},
2324
"dependencies": {
2425
"@aws-sdk/client-s3": "^3.1019.0",
@@ -63,7 +64,10 @@
6364
"rxjs": "^7.8.1",
6465
"swagger-ui-express": "^5.0.1",
6566
"typeorm": "^0.3.28",
66-
"uuid": "^13.0.0"
67+
"uuid": "^13.0.0",
68+
"@nestjs/bull": "^0.9.0",
69+
"bullmq": "^5.2.0",
70+
"@bull-board/express": "^4.0.0"
6771
},
6872
"devDependencies": {
6973
"@eslint/eslintrc": "^3.2.0",

0 commit comments

Comments
 (0)