# Quickstart

> Accept your first test payment in a few minutes — no real money, no payment account.

> **Note:** **TEST MODE — no real money moves.** Everything on this page uses a test key. You can click *Simulate successful payment* without worry.

## The whole idea

1. **Use test mode.** It needs no bKash or Nagad account.
2. **Create a test secret key** in the dashboard.
3. The secret key lives **on your server**, never in browser or app code.
4. **Create a Payment** from your server.
5. **Open its `checkoutUrl`** for the customer.
6. **Simulate success** with one call.
7. **Verify the webhook** Devzity sends your server.

Building a mobile app? Your app talks to your small backend, and that backend talks to Devzity — start with your platform below.

## Choose your platform

Pick your platform: REST (any language), JavaScript / Next.js, Android, iOS, React Native or Flutter. Each has its own page below.

The steps below are for **Node.js, Next.js and any server that speaks HTTP**. Other platforms follow the same six steps in their own guide.

## 1. Create a test secret key

In the dashboard open **Developers → API keys → Create secret key**, choose **Test**, and copy the key (it starts with `dz_test_sk_`). It is shown once. Keep it on your server:

```bash
export DEVZITY_SECRET_KEY=dz_test_sk_...
export DEVZITY_API_URL=https://api.devzity.com
```

## 2. Get the SDK (or use REST)

**Node.js**

**SDK preview:** This package is not publicly published yet, so there is nothing to install from a registry.

- Use the REST API (curl or plain fetch). See REST & other languages.
- Or build the package from the repository (`pnpm pack` in sdk/javascript) for local development.

**Next.js**

**Next.js helper preview:** This package is not publicly published yet, so there is nothing to install from a registry.

- Use the REST API from a Route Handler, or build the package locally (`pnpm pack` in sdk/nextjs).

You can always skip the SDK and call the [REST API](https://docs.devzity.com/v1/rest.md) directly — the steps are identical.

## 3. Create a payment

create-payment.mjs:

```js
// @devzity/sdk quickstart — run on your SERVER (Node.js 18+).
import { Devzity } from "@devzity/sdk";

const devzity = new Devzity({
  secretKey: process.env.DEVZITY_SECRET_KEY,
  baseUrl: process.env.DEVZITY_API_URL, // @preview-only — not needed once the SDK ships with the production URL built in
});

const payment = await devzity.payments.create({
  amount: 500,
});

console.log(payment.checkoutUrl);
```

`amount: 500` is fine for a simple value. For an exact amount such as ৳500.50, pass a string: `amount: "500.50"`. The SDK adds a secure idempotency key for you, so a retried request never creates a second payment.

Prefer plain HTTP?

**curl**

```bash
#!/usr/bin/env bash
# Create a TEST payment with plain curl. Needs a Test secret key from
# Developers → API keys (it starts with dz_test_sk_). No real money is involved.
set -euo pipefail
: "${DEVZITY_SECRET_KEY:?Set DEVZITY_SECRET_KEY to your dz_test_sk_... key}"
: "${DEVZITY_API_URL:?Set DEVZITY_API_URL to the Devzity API base URL you were given}"

curl "$DEVZITY_API_URL/v1/payments" \
  -X POST \
  -H "Authorization: Bearer $DEVZITY_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"amount":"500.00"}'
```

**Node.js (fetch)**

```js
// Plain Node.js (18+), no dependencies: create a TEST payment with fetch.
// Run on your SERVER — the secret key must never reach a browser or an app.
import { randomUUID } from "node:crypto";

const apiUrl = process.env.DEVZITY_API_URL; // the Devzity API base URL you were given
if (!apiUrl || !process.env.DEVZITY_SECRET_KEY) throw new Error("Set DEVZITY_API_URL and DEVZITY_SECRET_KEY");

const response = await fetch(`${apiUrl}/v1/payments`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DEVZITY_SECRET_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({ amount: "500.00" }),
});

const payment = await response.json();
if (!response.ok) {
  // Every error has a code, a message and a request_id to quote to support.
  throw new Error(`${payment.error.code}: ${payment.error.message} (${payment.error.request_id})`);
}
console.log(payment.checkout_url);
```

## 4. Open the `checkoutUrl`

Send your customer to `payment.checkoutUrl`: redirect them from your server, or return it to your mobile app to open ([Android](https://docs.devzity.com/v1/android.md), [iOS](https://docs.devzity.com/v1/ios.md)). The test checkout page shows a **TEST MODE** banner and a fake receiving number.

## 5. Simulate the payment

No real money moves in test mode. Complete the payment with one call — or press **Simulate successful payment** on the test checkout page:

**Node.js**

```js
await devzity.testHelpers.payments.succeed(payment.id)
```

**curl**

```bash
#!/usr/bin/env bash
# Complete a TEST payment without any real money (Test secret keys only).
# Usage: ./simulate-payment.sh <payment-id> [succeed|cancel]
set -euo pipefail
: "${DEVZITY_SECRET_KEY:?Set DEVZITY_SECRET_KEY to your dz_test_sk_... key}"
: "${DEVZITY_API_URL:?Set DEVZITY_API_URL to the Devzity API base URL you were given}"

curl "$DEVZITY_API_URL/v1/test_helpers/payments/$1/${2:-succeed}" \
  -X POST \
  -H "Authorization: Bearer $DEVZITY_SECRET_KEY"
```

## 6. Receive the webhook

In **Developers → Webhooks → Add endpoint** register your server's URL for the **Test** environment and copy its signing secret (`whsec_…`) into `DEVZITY_WEBHOOK_SECRET`.

**Node.js**

```js
// Receive Devzity webhooks with @devzity/sdk (Node.js 18+, no framework).
import { createServer } from "node:http";
import { DevzitySignatureVerificationError, verifyWebhook } from "@devzity/sdk";

// Demo only: Devzity delivers each event AT LEAST once, so remember the ids you have handled.
// In production use your database (a table with a unique event_id) instead of a Set.
const processed = new Set();

createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/devzity/webhook") {
    res.writeHead(404).end();
    return;
  }

  // 1. Read the RAW body — do not JSON.parse before verifying.
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks);

  // 2. Verify the signature. Never trust an unverified webhook.
  let event;
  try {
    event = verifyWebhook(rawBody, req.headers["devzity-signature"], process.env.DEVZITY_WEBHOOK_SECRET);
  } catch (error) {
    if (error instanceof DevzitySignatureVerificationError) {
      res.writeHead(400).end();
      return;
    }
    throw error;
  }

  // 3. Already handled this event? Acknowledge and stop — never fulfil twice.
  if (processed.has(event.id)) {
    res.writeHead(200).end();
    return;
  }

  // 4. Fulfil ONLY from a verified payment.paid event. Ignore anything you don't recognise.
  if (event.type === "payment.paid") {
    console.log("Fulfil the order for payment", event.data.object.id);
  }
  processed.add(event.id);

  // 5. Any 2xx acknowledges the event. Don't rely on delivery order.
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ received: true }));
}).listen(Number(process.env.PORT ?? 3000));
```

**Next.js**

```ts
import { verifyDevzityWebhook } from "@devzity/next";

// Demo only: Devzity delivers each event AT LEAST once. In production keep processed event ids in
// your database (a unique constraint on event.id) instead of in memory.
const processed = new Set<string>();

export async function POST(request: Request) {
  // Reads the RAW body and DEVZITY_WEBHOOK_SECRET; throws if the signature is not valid.
  const event = await verifyDevzityWebhook(request);

  if (processed.has(event.id)) return Response.json({ received: true }); // already handled

  if (event.type === "payment.paid") {
    // Fulfil the order for event.data.object.id — only from a verified payment.paid event.
  }
  processed.add(event.id);

  return Response.json({ received: true });
}
```

The example verifies the signature, ignores an event it has already handled, and fulfils only from `payment.paid`. Details: [Webhooks](https://docs.devzity.com/v1/webhooks.md).

## You're done

If everything worked you now have:

| Where | What you see |
| --- | --- |
| Your `console.log` | a `checkoutUrl` |
| The payment | `status: PAID` after step 5 |
| Your webhook endpoint | a verified `payment.paid` event with `livemode: false` |
| The dashboard → Developers → Webhooks | the delivery marked **Delivered** |

That is a complete integration. Next: [Going live](https://docs.devzity.com/v1/going-live.md) when you're ready for real payments, or [Payments](https://docs.devzity.com/v1/payments.md) and [Checkout](https://docs.devzity.com/v1/checkout.md) for the details.
