Scripts_ API Documentation

Everything you need to integrate the Scripts Pay sandbox into your application.

REST API
Webhooks
VietQR

Getting Started

All API requests are made to the following base URL. In sandbox mode, this points to your local or hosted NestJS instance.

BASE_URL = https://api.scriptspay.dev/v1

# Or for local development:
BASE_URL = http://localhost:3001/v1

All endpoints return JSON. Amounts are in the smallest currency unit (e.g., VND has no decimal subdivision, so 10000 = 10,000 VND).

Authentication

Authenticate by including your Supabase JWT in the Authorization header. You can obtain a token by signing in through the dashboard.

curl -X POST https://api.scriptspay.dev/v1/payment-intents \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <YOUR_JWT_TOKEN>" \
  -H "Idempotency-Key: unique-request-id-123" \
  -d '{
    "amount": 10000,
    "currency": "VND",
    "method": "QR",
    "merchantId": "your-merchant-profile-id"
  }'
Important: All mutation endpoints require an Idempotency-Key header. Duplicate keys within 24 hours return the cached response without re-processing.

Webhook Verification

Webhook payloads are signed with HMAC-SHA256. Verify the Scripts-Signature header on every delivery.

// Header format: Scripts-Signature: t=<timestamp>,v1=<signature>

const crypto = require('crypto');

function verifyWebhook(body, header, secret) {
  const [tPart, v1Part] = header.split(',');
  const t = tPart.replace('t=', '');
  const v1 = v1Part.replace('v1=', '');

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${JSON.stringify(body)}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(v1), Buffer.from(expected)
  );
}

Magic Test Data

In sandbox mode, the amount field on a Payment Intent determines the mock outcome:

AmountBehaviorWebhook Event
10,000 VND
Instant Success
payment_intent.succeeded
40,000 VND
Insufficient Funds
payment_intent.failed
50,408 VND
Bank Timeout (30s)
Resolves randomly (70% success, 30% fail)

Any other amount defaults to instant success.

React Integration

Drop the checkout button into your React app to create a payment intent and redirect to the hosted checkout page.

import { useState } from 'react';

function ScriptsCheckoutButton({ amount, merchantId }) {
  const [loading, setLoading] = useState(false);

  async function handleCheckout() {
    setLoading(true);
    const res = await fetch('https://api.scriptspay.dev/v1/payment-intents', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer <YOUR_SECRET_KEY>',
        'Idempotency-Key': crypto.randomUUID(),
      },
      body: JSON.stringify({
        amount,
        currency: 'VND',
        method: 'QR',
        merchantId,
      }),
    });

    const { id } = await res.json();
    // Redirect to hosted checkout
    window.location.href = `https://app.scriptspay.dev/checkout/${id}`;
  }

  return (
    <button onClick={handleCheckout} disabled={loading}>
      {loading ? 'Processing...' : `Pay ${amount.toLocaleString()} VND`}
    </button>
  );
}

export default ScriptsCheckoutButton;