Skip to content

Commit 1abbec5

Browse files
committed
feat(sdk): webhooks
Initial draft of a webhook capabilities. The goal of this PR is to test the interfacing between the webhooks service that we are working on and the SDKs.
1 parent f38f21a commit 1abbec5

10 files changed

Lines changed: 1687 additions & 25 deletions

File tree

codegen/src/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export async function generateIndex(
2828
export type { APIConfig } from './client'
2929
export { APIError, SumUpError } from './core'
3030
export type { RequestOptions } from './core'
31+
export * from './webhooks'
3132
export * from './types'
3233
`);
3334

examples/webhooks/index.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Minimal Express server that verifies and parses SumUp webhooks.
3+
*
4+
* Required environment variables:
5+
* - `SUMUP_WEBHOOK_SECRET`
6+
*
7+
* Optional environment variables:
8+
* - `SUMUP_API_KEY`
9+
* - `PORT`
10+
*/
11+
12+
import SumUp, {
13+
CheckoutCreatedEvent,
14+
SIGNATURE_HEADER,
15+
TIMESTAMP_HEADER,
16+
WebhookPayloadError,
17+
WebhookSignatureError,
18+
WebhookSignatureExpiredError,
19+
WebhookTimestampError,
20+
} from "@sumup/sdk";
21+
import express from "express";
22+
23+
const webhookSecret = process.env.SUMUP_WEBHOOK_SECRET;
24+
if (!webhookSecret) {
25+
throw new Error(
26+
"Missing SUMUP_WEBHOOK_SECRET environment variable. Please configure it before starting the example.",
27+
);
28+
}
29+
30+
const client = new SumUp({
31+
apiKey: process.env.SUMUP_API_KEY,
32+
});
33+
const webhooks = client.webhookHandler(webhookSecret);
34+
35+
const app = express();
36+
app.use("/webhooks", express.raw({ type: "*/*" }));
37+
38+
/** Receives webhook deliveries, verifies them, and logs the event. */
39+
app.post("/webhooks", async (req, res) => {
40+
const signature = req.header(SIGNATURE_HEADER) ?? "";
41+
const timestamp = req.header(TIMESTAMP_HEADER) ?? "";
42+
const body = req.body instanceof Buffer ? req.body.toString("utf8") : "";
43+
44+
try {
45+
const event = await webhooks.verifyAndParse(body, signature, timestamp);
46+
47+
console.info("Received webhook", {
48+
id: event.id,
49+
objectId: event.object.id,
50+
type: event.type,
51+
});
52+
53+
if (event instanceof CheckoutCreatedEvent) {
54+
const checkout = await event.fetchObject();
55+
console.info("Fetched checkout status", checkout.status);
56+
}
57+
58+
res.status(204).end();
59+
} catch (error) {
60+
if (
61+
error instanceof WebhookSignatureError ||
62+
error instanceof WebhookSignatureExpiredError ||
63+
error instanceof WebhookTimestampError
64+
) {
65+
res.status(401).send("Invalid webhook signature");
66+
return;
67+
}
68+
69+
if (error instanceof WebhookPayloadError) {
70+
res.status(400).send("Invalid webhook payload");
71+
return;
72+
}
73+
74+
console.error("Unhandled webhook error", error);
75+
res.status(500).send("Internal Server Error");
76+
}
77+
});
78+
79+
const port = Number(process.env.PORT ?? 3000);
80+
app.listen(port, () => {
81+
console.info(`Listening for webhooks on http://localhost:${port}/webhooks`);
82+
});

0 commit comments

Comments
 (0)