Merchant docs

Custom store integration

How to start a Novupay checkout from your own website or backend — without WooCommerce.

Important

Novupay does not expose /api/card2pay/start on portal.novupay.com. That path would live on your store if you want it — your backend builds the encrypted hash and redirects the customer to Novupay.

Flow

  1. Customer clicks Pay on your site.
  2. Your server creates a JSON payload (order id, amount, currency, email, callback URL…).
  3. Your server encrypts it into a hash using your API secret.
  4. Browser redirects to https://portal.novupay.com/pay/h/<hash>
  5. Novupay decrypts the hash and immediately redirects the browser to InfinitePay.
  6. When paid, Novupay POSTs order.paid to your callback URL.

Credentials

Create a site in Dashboard → Sites (API):

  • api_key — starts with c2p_
  • api_secret — 64 hex chars, used for encryption + callback signature verification

Payload JSON (before encryption)

Amount and currency go inside this JSON — not as separate query params on a Novupay API URL.

{
  "order_id": "12345",
  "amount": 240.00,
  "currency": "USD",
  "email": "customer@example.com",
  "description": "Order 12345",
  "return_url": "https://your-store.com/order/12345/thank-you",
  "callback_url": "https://your-store.com/api/card2pay/webhook",
  "ts": 1710000000000
}
FieldRequiredNotes
order_idYesYour order id (string)
amountYesNumber, e.g. 240.00
currencyYesISO code: USD, EUR, …
emailStrongly recommendedRequired for card checkout (PayGate)
callback_urlRecommendedWhere we POST when paid (can also set on site in dashboard)
return_urlOptionalRedirect customer back after payment

Hash format

c2p1.<api_key>.<iv_b64url>.<ciphertext_b64url>.<tag_b64url>
  • AES-256-GCM
  • Key = SHA-256(api_secret) as raw 32 bytes
  • IV = 12 random bytes
  • AAD (additional authenticated data) = api_key string
  • Plaintext = JSON payload UTF-8

Checkout URL (what the customer opens)

https://portal.novupay.com/pay/h/c2p1.c2p_abc123....

HTTP GET redirect (303). Novupay creates the order and sends the browser straight to InfinitePay. The store never calls InfinitePay and never handles its API key.

Example: your /api/card2pay/start (Node.js)

This runs on your server, not on Novupay:

import crypto from "node:crypto";

const API_KEY = process.env.CARD2PAY_API_KEY;
const API_SECRET = process.env.CARD2PAY_API_SECRET;
const CARD2PAY_URL = "https://portal.novupay.com";

function b64url(buf) {
  return buf.toString("base64url");
}

function encodeHash(payload) {
  const key = crypto.createHash("sha256").update(API_SECRET, "utf8").digest();
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
  cipher.setAAD(Buffer.from(API_KEY, "utf8"));
  const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  const tag = cipher.getAuthTag();
  return ["c2p1", API_KEY, b64url(iv), b64url(ciphertext), b64url(tag)].join(".");
}

// POST /api/card2pay/start  { orderId, amount, currency, email }
export function startCheckout(req, res) {
  const payload = {
    order_id: String(req.body.orderId),
    amount: Number(req.body.amount),
    currency: String(req.body.currency || "USD").toUpperCase(),
    email: req.body.email,
    callback_url: "https://your-store.com/api/card2pay/webhook",
    return_url: `https://your-store.com/orders/${req.body.orderId}`,
    ts: Date.now(),
  };
  const hash = encodeHash(payload);
  res.redirect(302, `${CARD2PAY_URL}/pay/h/${hash}`);
}

Example: PHP (same as WooCommerce plugin)

$payload = [
  'order_id'     => (string) $orderId,
  'amount'       => 240.00,
  'currency'     => 'USD',
  'email'        => 'customer@example.com',
  'callback_url' => 'https://your-store.com/api/card2pay/webhook',
  'return_url'   => 'https://your-store.com/thank-you',
  'ts'           => round(microtime(true) * 1000),
];

$key = hash('sha256', $apiSecret, true);
$iv  = random_bytes(12);
$tag = '';
$ciphertext = openssl_encrypt(
  json_encode($payload),
  'aes-256-gcm',
  $key,
  OPENSSL_RAW_DATA,
  $iv,
  $tag,
  $apiKey,
  16
);

$b64url = fn($bin) => rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
$hash = implode('.', ['c2p1', $apiKey, $b64url($iv), $b64url($ciphertext), $b64url($tag)]);

header('Location: https://portal.novupay.com/pay/h/' . $hash, true, 302);
exit;

When payment is confirmed

Novupay POSTs JSON to your callback_url. Verify header x-card2pay-signature: sha256=… with HMAC-SHA256 of the raw body using your api_secret.

Order status & webhook details →

Test without coding

In Dashboard → Sites (API), use Test checkout hash — it builds a valid hash with your amount and opens checkout.

Common mistakes

  • Calling portal.novupay.com/api/…/start — does not exist
  • Putting amount/currency in query string instead of encrypted JSON
  • Building PayGate pay.php URLs yourself — Novupay does that after hash decode
  • Missing email — card checkout returns 400
  • Wrong api_secret → hash decrypt fails → pay/error
Troubleshooting →