# I only have an app

> You have an Android, iOS, React Native or Flutter app and no server? You still must not put a Devzity secret key in it — you need one tiny endpoint.

```text
Your app  ──▶  your small backend  ──(Devzity secret key)──▶  Devzity
   ▲                  │
   └──── checkoutUrl ─┘
```

> **Warning:** **You still must not put a Devzity secret key inside your app.** An app can be unpacked and the key read by anyone. You only need a **tiny trusted server endpoint** to create the Payment — about twenty lines.

## The rule that keeps you safe: the server decides the price

The app sends an **order id**. The server loads that order and decides the amount itself. Never let the app send the amount to forward to Devzity — a tampered app could change ৳1,250 to ৳1.

> **Note:** A tutorial may accept an amount from the client to keep things short. Treat that as **demo only**. For real orders, calculate the amount on your server.

Pick the stack you already know. Each example creates the payment for an order and returns only the `checkout_url`.

**Node.js**

```js
// The tiny trusted server a mobile app needs (Android, iOS, React Native, Flutter).
// The app calls THIS; only this server holds the Devzity secret key.
//
// The app sends an ORDER ID. The server looks up the order and decides the amount itself —
// never trust an amount sent by the app (it could be changed to ৳1).
import { createServer } from "node:http";
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
});

// Stand-in for YOUR database of orders.
const orders = new Map([["order-1001", { total: "1250.00", status: "AWAITING_PAYMENT", paymentId: null }]]);

const send = (res, status, body) => res.writeHead(status, { "Content-Type": "application/json" }).end(JSON.stringify(body));

createServer(async (req, res) => {
  try {
    const pay = req.method === "POST" && req.url?.match(/^\/orders\/([\w-]+)\/pay$/);
    if (pay) {
      const order = orders.get(pay[1]);
      if (!order) return send(res, 404, { error: "order_not_found" });
      if (order.status === "PAID") return send(res, 409, { error: "already_paid" });

      const payment = await devzity.payments.create(
        {
          amount: order.total, // decided by the server, from the order
          externalId: pay[1],
          returnUrl: "myapp://payment-return", // your app's link
        },
        { idempotencyKey: `pay-${pay[1]}` }, // the same order never creates two payments
      );
      order.paymentId = payment.id;
      return send(res, 200, { checkout_url: payment.checkoutUrl });
    }

    const status = req.method === "GET" && req.url?.match(/^\/orders\/([\w-]+)\/status$/);
    if (status) {
      const order = orders.get(status[1]);
      if (!order) return send(res, 404, { error: "order_not_found" });
      // The authoritative answer: ask Devzity (a verified payment.paid webhook would update this too).
      if (order.paymentId) {
        const payment = await devzity.payments.retrieve(order.paymentId);
        if (payment.status === "PAID") order.status = "PAID";
      }
      return send(res, 200, { status: order.status });
    }
    send(res, 404, { error: "not_found" });
  } catch (error) {
    console.error(error);
    send(res, 500, { error: "server_error" });
  }
}).listen(Number(process.env.PORT ?? 3000));
```

**Next.js**

```ts
import { devzity } from "@/lib/devzity";

// Stand-in for YOUR database of orders.
const orders = new Map([["order-1001", { total: "1250.00" }]]);

// POST /api/orders/pay  { "orderId": "order-1001" }  →  { checkoutUrl }
// The app sends an ORDER ID; the server decides the amount. Never accept an amount from the app.
export async function POST(request: Request) {
  const { orderId } = (await request.json()) as { orderId?: string };
  const order = orderId ? orders.get(orderId) : undefined;
  if (!orderId || !order) return Response.json({ error: "order_not_found" }, { status: 404 });

  const payment = await devzity.payments.create(
    { amount: order.total, externalId: orderId, returnUrl: "myapp://payment-return" },
    { idempotencyKey: `pay-${orderId}` },
  );
  return Response.json({ checkoutUrl: payment.checkoutUrl });
}
```

**Spring Boot**

```java
package com.example.devzity;

import java.math.BigDecimal;
import java.util.Map;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * The endpoint a mobile app calls. The app sends an ORDER ID; the server decides the amount from its own
 * order — never from anything the app sends.
 */
@RestController
public class OrderPaymentController {

    // Stand-in for YOUR database of orders.
    private static final Map<String, BigDecimal> ORDER_TOTALS = Map.of("order-1001", new BigDecimal("1250.00"));

    private final DevzityService devzity;

    public OrderPaymentController(DevzityService devzity) {
        this.devzity = devzity;
    }

    @PostMapping("/orders/{orderId}/pay")
    public ResponseEntity<Map<String, String>> pay(@PathVariable String orderId) {
        BigDecimal total = ORDER_TOTALS.get(orderId);
        if (total == null) return ResponseEntity.notFound().build();
        return ResponseEntity.ok(Map.of("checkout_url", devzity.createPayment(total.toPlainString())));
    }
}
```

**Laravel**

```php
<?php
// app/Http/Controllers/OrderPaymentController.php (Laravel 10+)
// POST /orders/{orderId}/pay — the app sends an ORDER ID; the server decides the amount.
namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Support\Facades\Http;

class OrderPaymentController extends Controller
{
    public function pay(string $orderId)
    {
        $order = Order::where('id', $orderId)->where('user_id', auth()->id())->firstOrFail();
        abort_if($order->status === 'paid', 409, 'already_paid');

        $response = Http::withToken(config('services.devzity.secret'))
            ->withHeaders(['Idempotency-Key' => "pay-{$order->id}"])   // the same order never creates two payments
            ->post(config('services.devzity.url') . '/v1/payments', [
                'amount' => number_format($order->total, 2, '.', ''),    // decided by the server
                'external_id' => (string) $order->id,
                'return_url' => 'myapp://payment-return',
            ])
            ->throw();

        return response()->json(['checkout_url' => $response->json('checkout_url')]);
    }
}
```

The Node.js example also shows the second endpoint every app needs: `GET /orders/:id/status`, which answers from **your** records after asking Devzity. Deploy any of these where you like — a small server, a serverless function, or a Next.js route.

## What the app does

1. Sends the order id to your backend and receives `checkout_url`.
2. Opens it: `DevzityCheckout.open(...)` on [Android](https://docs.devzity.com/v1/android.md) or `DevzityCheckout.present(...)` on [iOS](https://docs.devzity.com/v1/ios.md); with the system browser on [React Native](https://docs.devzity.com/v1/react-native.md) and [Flutter](https://docs.devzity.com/v1/flutter.md).
3. Comes back through your return link, then **asks your backend for the order's status**.

> **Warning:** The return link's `devzity_status` is only a hint. Never ship or unlock anything because the app saw `PAID` — refresh the order from your backend.

Keep the order id in your app's saved state, so you can ask for the status even if the customer never returns through the link.
