Skip to content

Commit eee84a7

Browse files
authored
Merge pull request #455 from Gogo-Eng/docs/mdx-migration
docs: migrate documentation to MDX with syntax highlighting
2 parents 7874691 + e95db4b commit eee84a7

7 files changed

Lines changed: 420 additions & 17 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# How to use the API
2+
3+
This guide mirrors the routes currently implemented in the backend of this repository.
4+
5+
## Base URL
6+
7+
During local development, the frontend defaults to:
8+
9+
```text
10+
http://localhost:4000
11+
```
12+
13+
## 1. Register a merchant
14+
15+
Create a merchant and receive both an API key and a webhook secret.
16+
17+
**Endpoint**
18+
19+
```http
20+
POST /api/register-merchant
21+
```
22+
23+
**Example**
24+
25+
```bash
26+
curl -X POST http://localhost:4000/api/register-merchant \
27+
-H "Content-Type: application/json" \
28+
-d '{
29+
"email": "merchant@example.com",
30+
"business_name": "Example Store",
31+
"notification_email": "ops@example.com"
32+
}'
33+
```
34+
35+
**Response fields to save**
36+
37+
- `merchant.api_key`
38+
- `merchant.webhook_secret`
39+
- `merchant.id`
40+
41+
## 2. Create a payment link
42+
43+
All merchant-protected endpoints use the `x-api-key` header.
44+
45+
**Endpoint**
46+
47+
```http
48+
POST /api/create-payment
49+
```
50+
51+
**Headers**
52+
53+
```text
54+
x-api-key: sk_...
55+
Content-Type: application/json
56+
Idempotency-Key: 3f0d65e1-27b8-4b28-8f2f-8a6f9fd9d7d9
57+
```
58+
59+
`Idempotency-Key` is optional, but recommended. The backend caches successful duplicate requests for 24 hours.
60+
61+
**Example**
62+
63+
```bash
64+
curl -X POST http://localhost:4000/api/create-payment \
65+
-H "Content-Type: application/json" \
66+
-H "x-api-key: sk_your_api_key" \
67+
-H "Idempotency-Key: 3f0d65e1-27b8-4b28-8f2f-8a6f9fd9d7d9" \
68+
-d '{
69+
"amount": 25,
70+
"asset": "XLM",
71+
"recipient": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
72+
"description": "Order #2048",
73+
"webhook_url": "https://merchant.example/webhooks/stellar"
74+
}'
75+
```
76+
77+
**Typical success response**
78+
79+
```json
80+
{
81+
"payment_id": "6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f",
82+
"payment_link": "http://localhost:3000/pay/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f",
83+
"status": "pending",
84+
"branding_config": {
85+
"primary_color": "#5ef2c0",
86+
"secondary_color": "#b8ffe2",
87+
"background_color": "#050608"
88+
}
89+
}
90+
```
91+
92+
## 3. Check payment status
93+
94+
Use the public status endpoint to read the latest payment state.
95+
96+
**Endpoint**
97+
98+
```http
99+
GET /api/payment-status/:id
100+
```
101+
102+
**Example**
103+
104+
```bash
105+
curl http://localhost:4000/api/payment-status/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f
106+
```
107+
108+
## 4. Verify the payment on Stellar
109+
110+
After the customer submits payment, verify it against the Stellar network.
111+
112+
**Endpoint**
113+
114+
```http
115+
POST /api/verify-payment/:id
116+
```
117+
118+
If the payment is found, the API marks it as `confirmed`, stores the `tx_id`, emits the merchant socket event, and sends the webhook.
119+
120+
**Example**
121+
122+
```bash
123+
curl -X POST http://localhost:4000/api/verify-payment/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f
124+
```
125+
126+
## 5. List merchant payments
127+
128+
Read recent payments for the authenticated merchant.
129+
130+
**Endpoint**
131+
132+
```http
133+
GET /api/payments?page=1&limit=10
134+
```
135+
136+
**Headers**
137+
138+
```text
139+
x-api-key: sk_...
140+
```
141+
142+
## 6. Test webhook delivery
143+
144+
If you already stored a webhook URL for the merchant, you can send a signed test event using:
145+
146+
```http
147+
POST /api/webhooks/test
148+
```
149+
150+
If you want to ping an arbitrary URL directly, the repo also exposes:
151+
152+
```http
153+
POST /api/test-webhook
154+
```
155+
156+
with:
157+
158+
```json
159+
{
160+
"webhook_url": "https://merchant.example/webhooks/stellar"
161+
}
162+
```
163+
164+
## Notes
165+
166+
- Merchant auth in this codebase uses `x-api-key`, not `Authorization: Bearer ...`.
167+
- The create-payment flow supports optional `webhook_url`, `memo`, `memo_type`, and `branding_overrides`.
168+
- Webhook events are signed with the merchant webhook secret using `HMAC-SHA256`.
169+
170+
Continue with the HMAC guide in `/docs/hmac-signatures` to verify those webhook requests correctly.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# How to verify HMAC signatures
2+
3+
Webhook signing in this repository lives in `backend/src/lib/webhooks.js`.
4+
5+
## Header format
6+
7+
When a webhook secret is available, the backend sends:
8+
9+
```text
10+
Stellar-Signature: sha256=<hex_digest>
11+
```
12+
13+
The digest is generated from the exact JSON string body using HMAC-SHA256.
14+
15+
## Signing logic used by the backend
16+
17+
The backend currently signs payloads with logic equivalent to:
18+
19+
```js
20+
import {createHmac} from "crypto";
21+
22+
const rawBody = JSON.stringify(payload);
23+
const digest = createHmac("sha256", webhookSecret)
24+
.update(rawBody)
25+
.digest("hex");
26+
27+
const header = `sha256=${digest}`;
28+
```
29+
30+
## Important verification rule
31+
32+
Verify the signature against the **raw request body**, not a re-serialized object created later in your handler.
33+
34+
If your framework parses JSON first and you rebuild it with a different key order or whitespace, the signature check can fail.
35+
36+
## Node.js example
37+
38+
```js
39+
import crypto from "node:crypto";
40+
import express from "express";
41+
42+
const app = express();
43+
44+
app.post(
45+
"/webhooks/stellar",
46+
express.raw({type: "application/json"}),
47+
(req, res) => {
48+
const secret = process.env.STELLAR_WEBHOOK_SECRET;
49+
const rawBody = req.body.toString("utf8");
50+
const incoming = req.get("Stellar-Signature") || "";
51+
52+
const expected = `sha256=${crypto
53+
.createHmac("sha256", secret)
54+
.update(rawBody)
55+
.digest("hex")}`;
56+
57+
const valid =
58+
incoming.length === expected.length &&
59+
crypto.timingSafeEqual(Buffer.from(incoming), Buffer.from(expected));
60+
61+
if (!valid) {
62+
return res.status(401).json({error: "Invalid webhook signature"});
63+
}
64+
65+
const payload = JSON.parse(rawBody);
66+
67+
if (payload.event === "payment.confirmed") {
68+
console.log("Confirmed payment:", payload.payment_id, payload.tx_id);
69+
}
70+
71+
res.status(204).end();
72+
}
73+
);
74+
```
75+
76+
## Example payload fields
77+
78+
The backend sends a `payment.confirmed` payload with fields like:
79+
80+
```json
81+
{
82+
"event": "payment.confirmed",
83+
"payment_id": "6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f",
84+
"amount": 25,
85+
"asset": "XLM",
86+
"asset_issuer": null,
87+
"recipient": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
88+
"tx_id": "stellar_tx_hash"
89+
}
90+
```
91+
92+
The test webhook route sends a similar payload with:
93+
94+
- `event: "payment.confirmed"`
95+
- `test: true`
96+
97+
## Retry behavior
98+
99+
If delivery fails, the backend schedules retries after:
100+
101+
- 10 seconds
102+
- 30 seconds
103+
- 60 seconds
104+
105+
Your webhook handler should therefore be:
106+
107+
- idempotent
108+
- fast to acknowledge
109+
- tolerant of duplicate deliveries
110+
111+
## Checklist
112+
113+
- Save the `webhook_secret` returned during merchant registration.
114+
- Read the raw request body before JSON parsing changes it.
115+
- Compute `HMAC-SHA256` over that exact raw body.
116+
- Compare against the `Stellar-Signature` header.
117+
- Reject invalid signatures with `401`.
118+
- Treat webhook events as retryable and idempotent.

frontend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"motion": "^12.38.0",
2525
"next": "^14.2.5",
2626
"next-intl": "^4.8.3",
27+
"next-mdx-remote": "^5.0.0",
2728
"next-themes": "^0.4.6",
2829
"qrcode.react": "^4.2.0",
2930
"react": "^18.3.1",
@@ -32,6 +33,8 @@
3233
"react-loading-skeleton": "^3.5.0",
3334
"react-simple-pull-to-refresh": "^1.3.4",
3435
"recharts": "^2.15.4",
36+
"rehype-prism-plus": "^2.0.0",
37+
"remark-gfm": "^4.0.0",
3538
"socket.io-client": "^4.8.1",
3639
"stellar-sdk": "^12.2.0",
3740
"zustand": "^5.0.12"

frontend/src/app/(public)/docs/[slug]/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { notFound } from "next/navigation";
2+
import { MDXRemote } from "next-mdx-remote/rsc";
23
import { docsManifest } from "@/lib/docs-manifest";
34
import { getDocBySlug } from "@/lib/docs";
45

@@ -50,10 +51,9 @@ export default async function DocPage({
5051
</p>
5152
</header>
5253

53-
<div
54-
className="docs-prose"
55-
dangerouslySetInnerHTML={{ __html: doc.html }}
56-
/>
54+
<div className="docs-prose">
55+
<MDXRemote {...doc.serialized} />
56+
</div>
5757
</article>
5858
);
5959
}

0 commit comments

Comments
 (0)