Payment Engine 30-Minute Quickstart
Run through a complete Payment Engine integration path in around 30 minutes.
This guide helps you run a complete Payment Engine flow in 30 minutes: create an order, redirect to hosted checkout or display the payment address, receive callbacks, and confirm final order status.
Acceptance Criteria
After completing this guide, you should be able to:
- Call
/api/v2/checkoutsuccessfully to create an order. - Get
cregis_id,checkout_url, andpayment_info[x].payment_address, then guide users to pay through hosted checkout or your own address-based payment page. - Verify callback
signand returnsuccess. - Determine order status using callbacks or
/api/v2/order/info.
Step 0: Prepare parameters (~5 minutes)
Prepare the following values first:
- BASE_URL: project-specific Base URL (copy it from Cregis App > Payment Engine > Project Settings > Base URL).
- PID: Payment Engine project ID.
- API_KEY: Payment Engine project API key.
- CALLBACK_URL: publicly reachable order callback endpoint.
Use the project-specific Base URL (copy it from Cregis App > Payment Engine > Project Settings > Base URL).
Do not reuse Base URL across projects; bind each Base URL with that project’s PID and API Key.
Store each project’s Base URL as environment variable BASE_URL and manage it together with that project’s PID and API_KEY.
Related docs:
Step 1: Prepare signing helper (~5 minutes)
All requests require nonce, timestamp, and sign. Use the Node.js / Python / PHP helpers below:
import crypto from 'crypto';
export function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}import hashlib
import secrets
import time
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body<?php
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}`timestamp` must be the current request-time millisecond timestamp. Do not reuse old values for long periods.
Step 2: Create order (~8 minutes)
Call /api/v2/checkout to create an order and get hosted checkout link and payment address.
curl -X POST "${BASE_URL}/api/v2/checkout" \
-H "Content-Type: application/json" \
-d '{
"pid": 1382528827416576,
"order_id": "merchant_order_20260417_0001",
"order_amount": "99.00",
"order_currency": "USD",
"payer_id": "user_10001",
"valid_time": 30,
"success_url": "https://merchant.example.com/pay/success",
"cancel_url": "https://merchant.example.com/pay/cancel",
"callback_url": "https://merchant.example.com/webhook/payment-engine",
"nonce": "n6k2q1",
"timestamp": 1776393000000,
"sign": "<calculated-signature>"
}'import crypto from 'crypto';
const BASE_URL = process.env.CREGIS_BASE_URL;
const API_KEY = process.env.CREGIS_API_KEY;
function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}
const body = buildSignedBody(API_KEY, {
"pid": 1382528827416576,
"order_id": "merchant_order_20260417_0001",
"order_amount": "99.00",
"order_currency": "USD",
"payer_id": "user_10001",
"valid_time": 30,
"success_url": "https://merchant.example.com/pay/success",
"cancel_url": "https://merchant.example.com/pay/cancel",
"callback_url": "https://merchant.example.com/webhook/payment-engine"
});
const response = await fetch(`${BASE_URL}/api/v2/checkout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
console.log(await response.json());import hashlib
import os
import secrets
import time
import requests
BASE_URL = os.environ['CREGIS_BASE_URL']
API_KEY = os.environ['CREGIS_API_KEY']
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body
body = build_signed_body(API_KEY, {
'pid': 1382528827416576,
'order_id': 'merchant_order_20260417_0001',
'order_amount': '99.00',
'order_currency': 'USD',
'payer_id': 'user_10001',
'valid_time': 30,
'success_url': 'https://merchant.example.com/pay/success',
'cancel_url': 'https://merchant.example.com/pay/cancel',
'callback_url': 'https://merchant.example.com/webhook/payment-engine',
})
headers = {'Content-Type': 'application/json'}
response = requests.post(f'{BASE_URL}/api/v2/checkout', json=body, headers=headers, timeout=10)
print(response.json())<?php
$baseUrl = getenv('CREGIS_BASE_URL');
$apiKey = getenv('CREGIS_API_KEY');
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}
$body = buildSignedBody($apiKey, [
'pid' => 1382528827416576,
'order_id' => 'merchant_order_20260417_0001',
'order_amount' => '99.00',
'order_currency' => 'USD',
'payer_id' => 'user_10001',
'valid_time' => 30,
'success_url' => 'https://merchant.example.com/pay/success',
'cancel_url' => 'https://merchant.example.com/pay/cancel',
'callback_url' => 'https://merchant.example.com/webhook/payment-engine',
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($body, JSON_UNESCAPED_SLASHES),
'timeout' => 10,
],
]);
echo file_get_contents($baseUrl . '/api/v2/checkout', false, $context);Focus on these response fields:
- data.
cregis_id: Cregis-side order ID (primary key for query/reconciliation). - data.
checkout_url: hosted checkout URL (for redirect or QR code). - data.
payment_info[x].payment_address: payment address for building your own address-based payment page.
Step 3: Open checkout or display payment address (~3 minutes)
After frontend gets checkout_url, you can redirect to Cregis hosted checkout:
window.location.href = checkoutUrl;If you build your own payment page, read payment_info[x].payment_address as the payment address and display it together with token, network, and receivable amount information.
During payment, one order may emit multiple event types such as paid, paid_partial, paid_over, and expired.
Step 4: Receive order callback (~7 minutes)
Payment Engine pushes order status changes to your callback_url. Important wrapper fields:
- event_name: fixed as
order - event_type:
paid/paid_partial/paid_over/expired/refunded/paid_remain - data: order and payment details object.
Example (minimal runnable):
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const API_KEY = process.env.CREGIS_API_KEY;
const processed = new Set(); // Use a database/Redis idempotency key in production
function buildSign(apiKey, payload) {
const canonical = Object.keys(payload)
.filter((k) => k !== 'sign' && payload[k] !== '' && payload[k] !== null && payload[k] !== undefined)
.sort()
.map((k) => `${k}${payload[k]}`)
.join('');
return crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
}
app.post('/webhook/payment-engine', (req, res) => {
const body = req.body || {};
if (buildSign(API_KEY, body) !== body.sign) {
return res.status(401).send('invalid sign');
}
const cregisId = body?.data?.cregis_id || 'no-cregis-id';
const txId = body?.data?.tx_id || body?.data?.refund_tx_id || 'no-tx';
const idemKey = `${body.event_name || 'order'}:${body.event_type || 'unknown'}:${cregisId}:${txId}`;
if (processed.has(idemKey)) {
return res.status(200).send('success');
}
processed.add(idemKey);
// TODO: apply your order state transition logic
// Example: paid -> complete; expired -> close; refunded -> refund completed
return res.status(200).send('success');
});
app.listen(8080, () => console.log('Payment Engine webhook server running on :8080'));import hashlib
import os
from flask import Flask, request
app = Flask(__name__)
API_KEY = os.environ['CREGIS_API_KEY']
processed = set() # Use a database/Redis idempotency key in production
def build_sign(api_key, payload):
canonical = ''.join(
f'{key}{payload[key]}'
for key in sorted(payload)
if key != 'sign' and payload[key] not in ('', None)
)
return hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
@app.post('/webhook/payment-engine')
def payment_engine_webhook():
body = request.get_json(silent=True) or {}
if build_sign(API_KEY, body) != body.get('sign'):
return 'invalid sign', 401
data = body.get('data') or {}
cregis_id = data.get('cregis_id', 'no-cregis-id')
tx_id = data.get('tx_id') or data.get('refund_tx_id') or 'no-tx'
idem_key = f"{body.get('event_name', 'order')}:{body.get('event_type', 'unknown')}:{cregis_id}:{tx_id}"
if idem_key in processed:
return 'success', 200
processed.add(idem_key)
# TODO: apply your order state transition logic
# Example: paid -> complete; expired -> close; refunded -> refund completed
return 'success', 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)<?php
$apiKey = getenv('CREGIS_API_KEY');
$body = json_decode(file_get_contents('php://input'), true) ?: [];
function buildSign(string $apiKey, array $payload): string
{
$sorted = $payload;
unset($sorted['sign']);
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
return strtolower(md5($apiKey . $canonical));
}
if (buildSign($apiKey, $body) !== ($body['sign'] ?? null)) {
http_response_code(401);
echo 'invalid sign';
exit;
}
$data = $body['data'] ?? [];
$cregisId = $data['cregis_id'] ?? 'no-cregis-id';
$txId = $data['tx_id'] ?? $data['refund_tx_id'] ?? 'no-tx';
$idemKey = ($body['event_name'] ?? 'order') . ':' . ($body['event_type'] ?? 'unknown') . ':' . $cregisId . ':' . $txId;
// Production: store $idemKey in a database/Redis unique index for idempotency.
// TODO: apply your order state transition logic
// Example: paid -> complete; expired -> close; refunded -> refund completed
http_response_code(200);
echo 'success';Callback success should be judged by: HTTP 200 and plain text response body exactly equal to success.Step 5: Query order status (~2 minutes)
Use strategy: callback-first + query fallback. Fallback endpoint: /api/v2/order/info.
curl -X POST "${BASE_URL}/api/v2/order/info" \
-H "Content-Type: application/json" \
-d '{
"pid": 1382528827416576,
"cregis_id": "po1420761885130752",
"nonce": "a1b2c3",
"timestamp": 1776393120000,
"sign": "<calculated-signature>"
}'import crypto from 'crypto';
const BASE_URL = process.env.CREGIS_BASE_URL;
const API_KEY = process.env.CREGIS_API_KEY;
function buildSignedBody(apiKey, bizBody) {
const body = {
...bizBody,
nonce: crypto.randomUUID().replaceAll('-', '').slice(0, 8),
timestamp: Date.now(),
};
const canonical = Object.keys(body)
.filter((k) => body[k] !== '' && body[k] !== null && body[k] !== undefined)
.sort()
.map((k) => `${k}${body[k]}`)
.join('');
const sign = crypto
.createHash('md5')
.update(`${apiKey}${canonical}`)
.digest('hex')
.toLowerCase();
return { ...body, sign };
}
const body = buildSignedBody(API_KEY, {
"pid": 1382528827416576,
"cregis_id": "po1420761885130752"
});
const response = await fetch(`${BASE_URL}/api/v2/order/info`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
console.log(await response.json());import hashlib
import os
import secrets
import time
import requests
BASE_URL = os.environ['CREGIS_BASE_URL']
API_KEY = os.environ['CREGIS_API_KEY']
def build_signed_body(api_key, biz_body):
body = {
**biz_body,
'nonce': secrets.token_hex(4),
'timestamp': int(time.time() * 1000),
}
canonical = ''.join(
f'{key}{body[key]}'
for key in sorted(body)
if body[key] not in ('', None)
)
body['sign'] = hashlib.md5(f'{api_key}{canonical}'.encode('utf-8')).hexdigest().lower()
return body
body = build_signed_body(API_KEY, {
'pid': 1382528827416576,
'cregis_id': 'po1420761885130752',
})
headers = {'Content-Type': 'application/json'}
response = requests.post(f'{BASE_URL}/api/v2/order/info', json=body, headers=headers, timeout=10)
print(response.json())<?php
$baseUrl = getenv('CREGIS_BASE_URL');
$apiKey = getenv('CREGIS_API_KEY');
function buildSignedBody(string $apiKey, array $bizBody): array
{
$body = $bizBody;
$body['nonce'] = bin2hex(random_bytes(4));
$body['timestamp'] = (int) round(microtime(true) * 1000);
$sorted = $body;
ksort($sorted, SORT_STRING);
$canonical = '';
foreach ($sorted as $key => $value) {
if ($value === '' || $value === null) {
continue;
}
$canonical .= $key . $value;
}
$body['sign'] = strtolower(md5($apiKey . $canonical));
return $body;
}
$body = buildSignedBody($apiKey, [
'pid' => 1382528827416576,
'cregis_id' => 'po1420761885130752',
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode($body, JSON_UNESCAPED_SLASHES),
'timeout' => 10,
],
]);
echo file_get_contents($baseUrl . '/api/v2/order/info', false, $context);Read data.status as follows:
- new: initial order status
- paid: order paid successfully
- expired: order timed out (exceeded valid_time)
- paid_over: payment exceeds the order amount
- paid_partial: payment is less than the order amount
- canceled: full refund completed
Go-live checklist
- Development and production PID/API_KEY are separated.
- Callback signature verification and idempotent processing are implemented.
- cregis_id is persisted as platform order primary key.
- Mapping from
event_typeto internal order states is defined. - Callback failure monitoring is ready.
Common FAQs (quick troubleshooting)
1) Order is created but user cannot open checkout page?
Check whether checkout_url is incorrectly encoded by frontend, or blocked by CSP / gateway.
2) Why do I receive multiple callbacks for one order?
Payment Engine may emit multiple event_type values for the same order (for example paid_partial then paid_remain). Process by event transitions and enforce idempotency.
3) Why does query status name differ from callback event name?
This is expected by design: callbacks represent event types, while query API returns current status. Maintain a unified internal state machine.
If you have completed this guide, next recommended reading: