Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions frontend/content/docs/api-guide.mdx
Original file line number Diff line number Diff line change
@@ -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.
118 changes: 118 additions & 0 deletions frontend/content/docs/hmac-signatures.mdx
Original file line number Diff line number Diff line change
@@ -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=<hex_digest>
```

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.
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/app/(public)/docs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -50,10 +51,9 @@ export default async function DocPage({
</p>
</header>

<div
className="docs-prose"
dangerouslySetInnerHTML={{ __html: doc.html }}
/>
<div className="docs-prose">
<MDXRemote {...doc.serialized} />
</div>
</article>
);
}
Loading
Loading