> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zennopay.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Stand up a sandbox integration end-to-end in about 10 minutes.

This walks through the minimum a partner backend needs to do to create a
payment intent and hand it off to the Zennopay SDK on mobile.

<Note>
  Sandbox keys and JWKS endpoint are issued manually during onboarding for v1.
  Email **[partners@zennopay.com](mailto:partners@zennopay.com)** to receive a sandbox key ID and signing
  secret.
</Note>

<Info>
  **Environments.** Code below shows the production URL
  (`https://api.zennopay.com`) for copy-paste readiness. While integrating,
  swap in the sandbox host (`https://api.sandbox.zennopay.com`) — sandbox
  HMAC keys are rejected by production and vice versa. See
  [Environments](/api-reference/environments).
</Info>

## 1. Get your credentials

You will receive:

* A **key ID** (e.g. `wizz_sandbox_2026q2`) and a 32-byte **signing secret**
  for HMAC-signed API calls.
* A **JWKS endpoint URL** you publish on your own domain
  (`https://your-domain/.well-known/jwks.json`), containing the RS256 public
  key your backend uses to sign session JWTs.

See [Authentication](/authentication) for the full key-management model.

## 2. Sign and send your first API call

Construct the canonical request string, HMAC-SHA256 it with your signing
secret, and send it with the four required headers.

<CodeGroup>
  ```bash cURL (Sandbox) theme={null}
  curl -X POST https://api.sandbox.zennopay.com/v1/payment_intents \
    -H "X-Zennopay-Key-Id: wizz_sandbox_2026q2" \
    -H "X-Zennopay-Timestamp: 2026-05-21T14:30:00Z" \
    -H "X-Zennopay-Nonce: a1b2c3d4e5f6789012345678abcdef00" \
    -H "X-Zennopay-Signature: <base64_hmac_sha256>" \
    -H "Content-Type: application/json" \
    -d '{"amount_usd_cents":345,"corridor":"th_promptpay"}'
  ```

  ```bash cURL (Production) theme={null}
  curl -X POST https://api.zennopay.com/v1/payment_intents \
    -H "X-Zennopay-Key-Id: wizz_prod_2026q2" \
    -H "X-Zennopay-Timestamp: 2026-05-21T14:30:00Z" \
    -H "X-Zennopay-Nonce: a1b2c3d4e5f6789012345678abcdef00" \
    -H "X-Zennopay-Signature: <base64_hmac_sha256>" \
    -H "Content-Type: application/json" \
    -d '{"amount_usd_cents":345,"corridor":"th_promptpay"}'
  ```

  ```typescript Node theme={null}
  import crypto from "node:crypto";

  // Pick the host for your environment. See /api-reference/environments.
  const ZENNOPAY_HOST =
    process.env.ZENNOPAY_HOST ?? "https://api.sandbox.zennopay.com";

  const secret = process.env.ZENNOPAY_SECRET!; // <your_secret>
  const method = "POST";
  const path = "/v1/payment_intents";
  const timestamp = new Date().toISOString();
  const nonce = crypto.randomBytes(16).toString("hex");
  const body = JSON.stringify({ amount_usd_cents: 345, corridor: "th_promptpay" });
  const bodyHash = crypto.createHash("sha256").update(body).digest("hex");

  const canonical = [method, path, timestamp, nonce, bodyHash].join("\n");
  const signature = crypto.createHmac("sha256", secret).update(canonical).digest("base64");

  const res = await fetch(ZENNOPAY_HOST + path, {
    method,
    headers: {
      "X-Zennopay-Key-Id": "wizz_sandbox_2026q2",
      "X-Zennopay-Timestamp": timestamp,
      "X-Zennopay-Nonce": nonce,
      "X-Zennopay-Signature": signature,
      "Content-Type": "application/json",
    },
    body,
  });
  ```
</CodeGroup>

You should receive a `200 OK` with an `intent_id`:

```json theme={null}
{
  "intent_id": "zp_AbCd1234EfGh5678",
  "status": "created",
  "amount_usd_cents": 345,
  "corridor": "th_promptpay"
}
```

## 3. Mint a session JWT

Sign a JWT with your **RS256 private key** that authorizes one user to
complete one intent. The session expires in 10 minutes max.

```json theme={null}
{
  "iss": "https://api.your-domain.com",
  "aud": "zennopay-checkout",
  "sub": "your_user_id_xyz",
  "iat": 1716305400,
  "exp": 1716305700,
  "jti": "0190a8b3-4c5d-7e6f-8a9b-c0d1e2f3a4b5",
  "zennopay:intent_id": "zp_AbCd1234EfGh5678",
  "zennopay:amount_usd_cents": 345,
  "zennopay:corridor": "th_promptpay",
  "zennopay:kyc_attestation": {
    "verified": true,
    "method": "your_kyc_v2",
    "verified_at": "2026-05-21T13:30:00Z"
  },
  "zennopay:sanctions_attestation": {
    "clean": true,
    "screened_at": "2026-05-21T14:25:00Z"
  }
}
```

Full claim documentation is in [Authentication → JWT claims](/authentication#client-session-jwt).

## 4. Hand off to the mobile SDK

Pass the `intent_id` and `jwt` to the Zennopay SDK on the user's device:

<CodeGroup>
  ```swift iOS theme={null}
  Zennopay.openCheckout(
    intentID: "zp_AbCd1234EfGh5678",
    jwt: "eyJhbGciOi...",
    returnScheme: "yourapp"
  )
  ```

  ```kotlin Android theme={null}
  Zennopay.openCheckout(
    intentId = "zp_AbCd1234EfGh5678",
    jwt = "eyJhbGciOi...",
    returnScheme = "yourapp",
  )
  ```
</CodeGroup>

The SDK opens a Zennopay-hosted checkout, the user scans + confirms, and the
SDK returns control to your app with a result callback.

See the platform-specific guides for the full SDK lifecycle:

<CardGroup cols={2}>
  <Card title="iOS SDK" icon="apple" href="/sdks/ios" />

  <Card title="Android SDK" icon="android" href="/sdks/android" />
</CardGroup>

## 5. Receive the webhook

After the payment settles (or fails), Zennopay POSTs a signed webhook to your
configured endpoint. See [Webhooks](/api-reference/webhooks) for the payload
shape and signature verification steps.

## Common errors

| HTTP | `error.code`            | Likely cause                                                                  |
| ---- | ----------------------- | ----------------------------------------------------------------------------- |
| 401  | `authentication_failed` | Bad HMAC signature, expired timestamp, replayed nonce, or IP not in allowlist |
| 401  | `jwt_invalid`           | JWT signature, audience, expiry, or `jti` (replay) check failed               |
| 400  | `invalid_corridor`      | Corridor must be `th_promptpay` or `vn_vietqr`                                |
| 422  | `amount_below_minimum`  | Minimum transaction value not met for the corridor                            |

All 401s use a generic message in the response body. Use the `request_id`
field to correlate with internal logs when contacting support.
