diff --git a/frontend/content/docs/api-guide.mdx b/frontend/content/docs/api-guide.mdx
new file mode 100644
index 00000000..98942f8b
--- /dev/null
+++ b/frontend/content/docs/api-guide.mdx
@@ -0,0 +1,170 @@
+# How to use the API
+
+This guide mirrors the routes currently implemented in the backend of this repository.
+
+## Base URL
+
+During local development, the frontend defaults to:
+
+```text
+http://localhost:4000
+```
+
+## 1. Register a merchant
+
+Create a merchant and receive both an API key and a webhook secret.
+
+**Endpoint**
+
+```http
+POST /api/register-merchant
+```
+
+**Example**
+
+```bash
+curl -X POST http://localhost:4000/api/register-merchant \
+ -H "Content-Type: application/json" \
+ -d '{
+ "email": "merchant@example.com",
+ "business_name": "Example Store",
+ "notification_email": "ops@example.com"
+ }'
+```
+
+**Response fields to save**
+
+- `merchant.api_key`
+- `merchant.webhook_secret`
+- `merchant.id`
+
+## 2. Create a payment link
+
+All merchant-protected endpoints use the `x-api-key` header.
+
+**Endpoint**
+
+```http
+POST /api/create-payment
+```
+
+**Headers**
+
+```text
+x-api-key: sk_...
+Content-Type: application/json
+Idempotency-Key: 3f0d65e1-27b8-4b28-8f2f-8a6f9fd9d7d9
+```
+
+`Idempotency-Key` is optional, but recommended. The backend caches successful duplicate requests for 24 hours.
+
+**Example**
+
+```bash
+curl -X POST http://localhost:4000/api/create-payment \
+ -H "Content-Type: application/json" \
+ -H "x-api-key: sk_your_api_key" \
+ -H "Idempotency-Key: 3f0d65e1-27b8-4b28-8f2f-8a6f9fd9d7d9" \
+ -d '{
+ "amount": 25,
+ "asset": "XLM",
+ "recipient": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
+ "description": "Order #2048",
+ "webhook_url": "https://merchant.example/webhooks/stellar"
+ }'
+```
+
+**Typical success response**
+
+```json
+{
+ "payment_id": "6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f",
+ "payment_link": "http://localhost:3000/pay/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f",
+ "status": "pending",
+ "branding_config": {
+ "primary_color": "#5ef2c0",
+ "secondary_color": "#b8ffe2",
+ "background_color": "#050608"
+ }
+}
+```
+
+## 3. Check payment status
+
+Use the public status endpoint to read the latest payment state.
+
+**Endpoint**
+
+```http
+GET /api/payment-status/:id
+```
+
+**Example**
+
+```bash
+curl http://localhost:4000/api/payment-status/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f
+```
+
+## 4. Verify the payment on Stellar
+
+After the customer submits payment, verify it against the Stellar network.
+
+**Endpoint**
+
+```http
+POST /api/verify-payment/:id
+```
+
+If the payment is found, the API marks it as `confirmed`, stores the `tx_id`, emits the merchant socket event, and sends the webhook.
+
+**Example**
+
+```bash
+curl -X POST http://localhost:4000/api/verify-payment/6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f
+```
+
+## 5. List merchant payments
+
+Read recent payments for the authenticated merchant.
+
+**Endpoint**
+
+```http
+GET /api/payments?page=1&limit=10
+```
+
+**Headers**
+
+```text
+x-api-key: sk_...
+```
+
+## 6. Test webhook delivery
+
+If you already stored a webhook URL for the merchant, you can send a signed test event using:
+
+```http
+POST /api/webhooks/test
+```
+
+If you want to ping an arbitrary URL directly, the repo also exposes:
+
+```http
+POST /api/test-webhook
+```
+
+with:
+
+```json
+{
+ "webhook_url": "https://merchant.example/webhooks/stellar"
+}
+```
+
+## Notes
+
+- Merchant auth in this codebase uses `x-api-key`, not `Authorization: Bearer ...`.
+- The create-payment flow supports optional `webhook_url`, `memo`, `memo_type`, and `branding_overrides`.
+- Webhook events are signed with the merchant webhook secret using `HMAC-SHA256`.
+
+Continue with the HMAC guide in `/docs/hmac-signatures` to verify those webhook requests correctly.
diff --git a/frontend/content/docs/hmac-signatures.mdx b/frontend/content/docs/hmac-signatures.mdx
new file mode 100644
index 00000000..4c4ad135
--- /dev/null
+++ b/frontend/content/docs/hmac-signatures.mdx
@@ -0,0 +1,118 @@
+# How to verify HMAC signatures
+
+Webhook signing in this repository lives in `backend/src/lib/webhooks.js`.
+
+## Header format
+
+When a webhook secret is available, the backend sends:
+
+```text
+Stellar-Signature: sha256=
+```
+
+The digest is generated from the exact JSON string body using HMAC-SHA256.
+
+## Signing logic used by the backend
+
+The backend currently signs payloads with logic equivalent to:
+
+```js
+import {createHmac} from "crypto";
+
+const rawBody = JSON.stringify(payload);
+const digest = createHmac("sha256", webhookSecret)
+ .update(rawBody)
+ .digest("hex");
+
+const header = `sha256=${digest}`;
+```
+
+## Important verification rule
+
+Verify the signature against the **raw request body**, not a re-serialized object created later in your handler.
+
+If your framework parses JSON first and you rebuild it with a different key order or whitespace, the signature check can fail.
+
+## Node.js example
+
+```js
+import crypto from "node:crypto";
+import express from "express";
+
+const app = express();
+
+app.post(
+ "/webhooks/stellar",
+ express.raw({type: "application/json"}),
+ (req, res) => {
+ const secret = process.env.STELLAR_WEBHOOK_SECRET;
+ const rawBody = req.body.toString("utf8");
+ const incoming = req.get("Stellar-Signature") || "";
+
+ const expected = `sha256=${crypto
+ .createHmac("sha256", secret)
+ .update(rawBody)
+ .digest("hex")}`;
+
+ const valid =
+ incoming.length === expected.length &&
+ crypto.timingSafeEqual(Buffer.from(incoming), Buffer.from(expected));
+
+ if (!valid) {
+ return res.status(401).json({error: "Invalid webhook signature"});
+ }
+
+ const payload = JSON.parse(rawBody);
+
+ if (payload.event === "payment.confirmed") {
+ console.log("Confirmed payment:", payload.payment_id, payload.tx_id);
+ }
+
+ res.status(204).end();
+ }
+);
+```
+
+## Example payload fields
+
+The backend sends a `payment.confirmed` payload with fields like:
+
+```json
+{
+ "event": "payment.confirmed",
+ "payment_id": "6aa64d44-faf1-41f0-a7e7-c8f9cce62f2f",
+ "amount": 25,
+ "asset": "XLM",
+ "asset_issuer": null,
+ "recipient": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
+ "tx_id": "stellar_tx_hash"
+}
+```
+
+The test webhook route sends a similar payload with:
+
+- `event: "payment.confirmed"`
+- `test: true`
+
+## Retry behavior
+
+If delivery fails, the backend schedules retries after:
+
+- 10 seconds
+- 30 seconds
+- 60 seconds
+
+Your webhook handler should therefore be:
+
+- idempotent
+- fast to acknowledge
+- tolerant of duplicate deliveries
+
+## Checklist
+
+- Save the `webhook_secret` returned during merchant registration.
+- Read the raw request body before JSON parsing changes it.
+- Compute `HMAC-SHA256` over that exact raw body.
+- Compare against the `Stellar-Signature` header.
+- Reject invalid signatures with `401`.
+- Treat webhook events as retryable and idempotent.
diff --git a/frontend/package.json b/frontend/package.json
index efcc74e8..c539391c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -24,6 +24,7 @@
"motion": "^12.38.0",
"next": "^14.2.5",
"next-intl": "^4.8.3",
+ "next-mdx-remote": "^5.0.0",
"next-themes": "^0.4.6",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
@@ -32,6 +33,8 @@
"react-loading-skeleton": "^3.5.0",
"react-simple-pull-to-refresh": "^1.3.4",
"recharts": "^2.15.4",
+ "rehype-prism-plus": "^2.0.0",
+ "remark-gfm": "^4.0.0",
"socket.io-client": "^4.8.1",
"stellar-sdk": "^12.2.0",
"zustand": "^5.0.12"
diff --git a/frontend/src/app/(public)/docs/[slug]/page.tsx b/frontend/src/app/(public)/docs/[slug]/page.tsx
index 2d6e6397..2a05a878 100644
--- a/frontend/src/app/(public)/docs/[slug]/page.tsx
+++ b/frontend/src/app/(public)/docs/[slug]/page.tsx
@@ -1,4 +1,5 @@
import { notFound } from "next/navigation";
+import { MDXRemote } from "next-mdx-remote/rsc";
import { docsManifest } from "@/lib/docs-manifest";
import { getDocBySlug } from "@/lib/docs";
@@ -50,10 +51,9 @@ export default async function DocPage({
-
+
+
+
);
}
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 5916c3d8..3ab15510 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -105,6 +105,91 @@ body {
padding: 0.15rem 0.45rem;
}
+/* Syntax highlighting for code blocks with rehype-prism-plus */
+.docs-prose pre {
+ overflow-x: auto;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(2, 6, 23, 0.82);
+ border-radius: 1rem;
+ padding: 1rem 1.1rem;
+ margin: 1.25rem 0;
+}
+
+.docs-prose pre code {
+ border: 0;
+ background: transparent;
+ color: #e2e8f0;
+ padding: 0;
+ font-size: 0.9em;
+ line-height: 1.5;
+}
+
+/* prism-plus syntax highlighting tokens */
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+ color: #7d8590;
+}
+
+.token.punctuation {
+ color: #e2e8f0;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+ color: #ffa657;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+ color: #7ee787;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+ color: #79c0ff;
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+ color: #ff7b72;
+}
+
+.token.function,
+.token.class-name {
+ color: #d2a8ff;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+ color: #ffa657;
+}
+
+/* Line highlighting for code blocks */
+.docs-prose pre > code {
+ display: block;
+}
+
+.token.line {
+ display: block;
+ min-height: 1.5em;
+}
+
.docs-prose pre {
overflow-x: auto;
border: 1px solid rgba(255, 255, 255, 0.08);
diff --git a/frontend/src/lib/docs.ts b/frontend/src/lib/docs.ts
index 6c620adf..386bb14d 100644
--- a/frontend/src/lib/docs.ts
+++ b/frontend/src/lib/docs.ts
@@ -1,12 +1,10 @@
import { promises as fs } from "node:fs";
import path from "node:path";
-import { marked } from "marked";
+import { serialize } from "next-mdx-remote/serialize";
+import remarkGfm from "remark-gfm";
+import rehypePrismPlus from "rehype-prism-plus";
import { docsManifest } from "@/lib/docs-manifest";
-marked.setOptions({
- gfm: true,
-});
-
export async function getDocBySlug(slug: string) {
const entry = docsManifest.find((doc) => doc.slug === slug);
@@ -14,13 +12,30 @@ export async function getDocBySlug(slug: string) {
return null;
}
- const filePath = path.join(process.cwd(), "content", "docs", entry.filename);
- const markdown = await fs.readFile(filePath, "utf8");
- const html = await marked.parse(markdown);
+ // Updated to use .mdx extension
+ const mdxFilename = entry.filename.replace(".md", ".mdx");
+ const filePath = path.join(process.cwd(), "content", "docs", mdxFilename);
+
+ try {
+ const mdxContent = await fs.readFile(filePath, "utf8");
+
+ // Serialize MDX content with plugins for syntax highlighting and GFM support
+ const serialized = await serialize(mdxContent, {
+ mdxOptions: {
+ remarkPlugins: [remarkGfm],
+ rehypePlugins: [
+ [rehypePrismPlus, { defaultLanguage: "bash", showLineNumbers: false }],
+ ],
+ },
+ });
- return {
- ...entry,
- markdown,
- html,
- };
+ return {
+ ...entry,
+ serialized,
+ filename: mdxFilename,
+ };
+ } catch (error) {
+ console.error(`Error loading doc ${slug}:`, error);
+ return null;
+ }
}
diff --git a/frontend/src/lib/mdx-config.ts b/frontend/src/lib/mdx-config.ts
new file mode 100644
index 00000000..3ca1c54b
--- /dev/null
+++ b/frontend/src/lib/mdx-config.ts
@@ -0,0 +1,12 @@
+import remarkGfm from 'remark-gfm';
+import { MDXRemoteSerializeOptions } from 'next-mdx-remote/rsc';
+import rehypePrismPlus from 'rehype-prism-plus';
+
+export const mdxOptions: MDXRemoteSerializeOptions = {
+ mdxOptions: {
+ remarkPlugins: [remarkGfm],
+ rehypePlugins: [
+ [rehypePrismPlus, { defaultLanguage: 'bash' }],
+ ],
+ },
+};