⚔ QRIS Payment Gateway - Real Implementation

šŸš€ Quick Start

Get started with Cutiezy API in minutes!

# Base URLhttps://cutiezy.id/zy-pay/api/v1/# Authentication HeaderX-API-Key: ak_7d8f9e1a2b3c4d5e6f7a8b9c0d1e2f3# First API Call (Create Payment)curl -X POST "https://cutiezy.id/zy-pay/api/v1/create-payment" \ -H "X-API-Key: your_api_key" \ -H "Content-Type: application/json" \ -d '{"amount": 10000, "description": "Test payment"}'
šŸ“Œ Note: Get your API key from Dashboard → API Keys

šŸ” Authentication

All API requests require authentication via API Key.

X-API-Key: ak_7d8f9e1a2b3c4d5e6f7a8b9c0d1e2f3

Getting Your API Key:

  1. Login to Dashboard
  2. Navigate to Settings → API Keys
  3. Click "Generate New API Key"
  4. āš ļø IMPORTANT: Save your API Secret immediately! (shown only once)
āš ļø Security Note:
  • Never expose your API key in client-side code
  • Use environment variables in production
  • Rotate keys regularly

ā±ļø Rate Limits

API rate limits to ensure service stability.

Endpoint Limit Window Description
POST /create-payment 1 request 3 seconds Prevent duplicate payments
GET /check-payment 10 requests 1 second Status checking
GET /mutations 5 requests 10 seconds History retrieval
All endpoints 100 requests 1 minute Global rate limit

šŸ’³ 1. Create Payment

Create a new QRIS payment request.

POST /create-payment
⚔ Real Implementation: This endpoint uses advisory locks and retry mechanisms to handle concurrent requests safely.

Request Body:

{ "amount": 10000, "description": "Payment for order #123", "webhook_url": "https://your-webhook.com/callback"}

Parameters:

Parameter Type Required Description Constraints
amount Required integer āœ… Payment amount in IDR 1,000 - 10,000,000
description Optional string āŒ Payment description Max 255 characters
webhook_url Optional string āŒ Webhook URL for notifications Valid HTTPS URL

Response (Success - 201 Created):

{ "success": true, "message": "Payment created successfully", "data": { "transaction_id": "QRIS20241230123045ABCDEF", "reference_id": 12345, "amount": 10000, "fee": 50, "total_amount": 10075, "payment_method": "QRIS", "qris_image_url": "https://cutiezy.id/qris/QRIS20241230123045ABCDEF.png", "qris_string": "00020101021126670014...", "expired_at": "2024-12-30 12:45:45", "status": "pending", "check_status_url": "https://cutiezy.id/zy-pay/api/v1/check-payment/QRIS20241230123045ABCDEF", "webhook_url": "https://your-webhook.com/callback", "created_at": "2024-12-30 12:30:45" }}

Fee Calculation:

// Fee = 0.5% of amount (rounded down)fee = Math.floor(amount * 0.5 / 100);// Total = Amount + Feetotal_amount = amount + fee;

šŸ” 2. Check Payment Status

Check payment status by transaction ID. Supports both URL path and query parameter.

GET /check-payment/{transaction_id}

OR: GET /check-payment?transaction_id={transaction_id}

OR: GET /check-payment?id={reference_id}

Response (Success - 200 OK):

{ "success": true, "data": { "transaction_id": "QRIS20241230123045ABCDEF", "reference_id": 12345, "amount": 10000, "fee": 50, "total_amount": 10075, "status": "paid", "payment_method": "QRIS", "qris_image_url": "https://cutiezy.id/qris/QRIS20241230123045ABCDEF.png", "expired_at": "2024-12-30 12:45:45", "paid_at": "2024-12-30 12:33:20", "settlement_at": "2024-12-30 20:33:20", "description": "Payment for order #123", "created_at": "2024-12-30 12:30:45", "updated_at": "2024-12-30 12:33:20" }}

Status Values:

pending - Waiting for payment - Payment received expired - Payment expired (30 minutes) cancel - Payment cancelled

āŒ 3. Cancel Payment

Cancel a pending payment before it expires or gets paid.

POST /cancel-payment

Request Body:

{ "transaction_id": "QRIS20241230123045ABCDEF"}

OR:

{ "id": 12345}

Response (Success - 200 OK):

{ "success": true, "message": "Payment cancelled successfully", "data": { "transaction_id": "QRIS20241230123045ABCDEF", "reference_id": 12345, "status": "cancelled", "cancelled_at": "2024-12-30 12:35:10" }}
āš ļø Restrictions:
  • Only pending payments can be cancelled
  • Cannot cancel paid or expired payments
  • Cancellation is permanent and irreversible

šŸ“Š 4. Transaction History (Mutations)

Get transaction history with filtering, pagination, and date range.

GET /mutations

Parameters:

Parameter Type Default Description
start_date string Current month 1st Format: YYYY-MM-DD
end_date string Today Format: YYYY-MM-DD
status string (all) Filter by status
page integer 1 Page number (≄1)
limit integer 50 Items per page (max 100)

Example Request:

GET /mutations?start_date=2024-12-01&end_date=2024-12-31&status=paid&page=1&limit=20

Response (Success - 200 OK):

{ "success": true, "data": [ { "transaction_id": "QRIS20241230123045ABCDEF", "reference_id": 12345, "amount": 10000, "fee": 50, "total_amount": 10075, "status": "paid", "payment_method": "QRIS", "qris_image_url": "https://cutiezy.id/qris/QRIS20241230123045ABCDEF.png", "expired_at": "2024-12-30 12:45:45", "paid_at": "2024-12-30 12:33:20", "description": "Payment for order #123", "created_at": "2024-12-30 12:30:45" } ], "pagination": { "total": 45, "page": 1, "limit": 20, "pages": 3 }, "filters": { "start_date": "2024-12-01", "end_date": "2024-12-31", "status": "paid" }}

šŸ‘¤ 5. User Profile & Statistics

Get user profile information, balance, and transaction statistics.

GET /profile

Response (Success - 200 OK):

{ "success": true, "data": { "user": { "user_id": 1, "username": "johndoe", "email": "john@example.com", "role": "user" }, "balance": { "available": 1500000.00, "currency": "IDR" }, "stats": { "total": { "pending": 2, "paid": 45, "expired": 3, "cancelled": 1, "total_amount": 4500000 }, "today": { "transactions": 5, "paid_amount": 250000 } }, "settings": { "fee_percentage": 0.5, "min_amount": 1000, "max_amount": 10000000 } }}

šŸ”„ 6. Webhook Notifications

Real-time notifications for payment events sent to your webhook URL.

Setup:

  1. Dashboard → Profile → Webhook Settings
  2. Enter your webhook URL and secret key
  3. Enable webhook notifications
  4. Or use webhook_url parameter in create-payment request

Signature Verification:

// Verify webhook signatureconst crypto = require('crypto');function verifySignature(payload, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}

Webhook Payload:

{ "event": "payment.paid", "data": { "transaction_id": "QRIS20241230123045ABCDEF", "reference_id": 12345, "amount": 10000, "fee": 50, "total_amount": 10075, "status": "paid", "payment_method": "QRIS", "description": "Payment for order #123", "paid_at": "2024-12-30 12:33:20", "expired_at": "2024-12-30 12:45:45", "settlement_at": "2024-12-30 20:33:20", "created_at": "2024-12-30 12:30:45" }, "timestamp": "2024-12-30 12:33:21"}

Webhook Headers:

Header Description Example
X-Webhook-Signature HMAC SHA256 signature sha256=abc123...
X-Webhook-Event Event type payment.paid
Content-Type Payload format application/json
User-Agent Sender identifier ZyPay-Webhook/1.0

Webhook Events:

Event Description Trigger Condition
payment.pending Payment created When user creates payment via API
payment.paid Payment successful When QRIS is scanned & paid
payment.expired Payment expired 30 minutes after creation (no payment)
payment.cancelled Payment cancelled User cancels via API or dashboard
šŸ’” Best Practices:
  • Always verify the signature
  • Respond with 200 OK within 5 seconds
  • Implement retry logic on your side
  • Log all webhook events

⚔ 7. Code Examples

PHP - Create Payment:

<?php$apiKey = "ak_7d8f9e1a2b3c4d5e6f7a8b9c0d1e2f3";$url = "https://cutiezy.id/zy-pay/api/v1/create-payment";$data = [ "amount" => 10000, "description" => "Order #123", "webhook_url" => "https://your-store.com/webhook/payment"];$ch = curl_init($url);curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($data), CURLOPT_HTTPHEADER => [ "X-API-Key: $apiKey", "Content-Type: application/json", "Accept: application/json" ]]);$response = curl_exec($ch);$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);$error = curl_error($ch);curl_close($ch);if ($httpCode === 201) { $result = json_decode($response, true); if ($result['success']) { echo "QRIS Image: " . $result['data']['qris_image_url']; echo "Transaction ID: " . $result['data']['transaction_id']; }} else { echo "Error $httpCode: $error";}?>

JavaScript/Node.js - Check Payment:

const axios = require('axios');class CutiezyAPI { constructor(apiKey) { this.apiKey = apiKey; this.baseURL = 'https://cutiezy.id/zy-pay/api/v1'; } async createPayment(amount, description, webhookUrl) { try { const response = await axios.post( `${this.baseURL}/create-payment`, { amount, description, webhook_url: webhookUrl }, { headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json' } } ); return response.data; } catch (error) { throw new Error(`Create payment failed: ${error.response?.data?.error || error.message}`); } } async checkPayment(transactionId) { try { const response = await axios.get( `${this.baseURL}/check-payment/${transactionId}`, { headers: { 'X-API-Key': this.apiKey } } ); return response.data.data; } catch (error) { throw new Error(`Check payment failed: ${error.response?.data?.error || error.message}`); } } async getMutations(options = {}) { const params = new URLSearchParams({ start_date: options.startDate || new Date().toISOString().split('T')[0], end_date: options.endDate || new Date().toISOString().split('T')[0], status: options.status || '', page: options.page || 1, limit: options.limit || 50 }); try { const response = await axios.get( `${this.baseURL}/mutations?${params}`, { headers: { 'X-API-Key': this.apiKey } } ); return response.data; } catch (error) { throw new Error(`Get mutations failed: ${error.response?.data?.error || error.message}`); } }}// Usageconst api = new CutiezyAPI('your_api_key_here');const payment = await api.createPayment(10000, 'Test payment');

Python - Webhook Handler:

from flask import Flask, request, jsonifyimport hmacimport hashlibimport jsonfrom datetime import datetimeapp = Flask(__name__)WEBHOOK_SECRET = "your_webhook_secret_here"def verify_signature(payload, signature): """Verify webhook signature""" expected = hmac.new( WEBHOOK_SECRET.encode('utf-8'), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected)@app.route('/webhook/payment', methods=['POST'])def payment_webhook(): try: # Get headers and payload signature = request.headers.get('X-Webhook-Signature', '') event = request.headers.get('X-Webhook-Event', '') payload = request.get_data() # Verify signature if not verify_signature(payload, signature): return jsonify({'error': 'Invalid signature'}), 401 data = json.loads(payload) # Process based on event type if event == 'payment.paid': transaction_id = data['data']['transaction_id'] amount = data['data']['amount'] # Update your database print(f"[{datetime.now()}] Payment received: {transaction_id} - Rp {amount:,}") # Return success response return jsonify({'status': 'success'}), 200 elif event == 'payment.expired': # Handle expired payment pass elif event == 'payment.cancelled': # Handle cancelled payment pass return jsonify({'status': 'processed'}), 200 except Exception as e: print(f"Webhook error: {str(e)}") return jsonify({'error': 'Processing failed'}), 500if __name__ == '__main__': app.run(port=5000, debug=True)

cURL - Complete Flow:

#!/bin/bashAPI_KEY="your_api_key_here"BASE_URL="https://cutiezy.id/zy-pay/api/v1"# 1. Create Paymentecho "Creating payment..."CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/create-payment" \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 10000, "description": "Test payment" }')TRANSACTION_ID=$(echo $CREATE_RESPONSE | jq -r '.data.transaction_id')echo "Transaction ID: $TRANSACTION_ID"echo "QRIS URL: $(echo $CREATE_RESPONSE | jq -r '.data.qris_image_url')"# 2. Check Payment Status (polling example)echo -e "\nChecking payment status..."for i in {1..30}; do STATUS_RESPONSE=$(curl -s -X GET "$BASE_URL/check-payment/$TRANSACTION_ID" \ -H "X-API-Key: $API_KEY") STATUS=$(echo $STATUS_RESPONSE | jq -r '.data.status') echo "Attempt $i: Status = $STATUS" if [ "$STATUS" = "paid" ]; then echo "āœ… Payment successful!" break elif [ "$STATUS" = "expired" ] || [ "$STATUS" = "cancel" ]; then echo "āŒ Payment failed: $STATUS" break fi sleep 10 # Check every 10 secondsdone# 3. Get Transaction Historyecho -e "\nGetting transaction history..."curl -s -X GET "$BASE_URL/mutations?limit=5" \ -H "X-API-Key: $API_KEY" | jq '.'

āš ļø 8. Error Codes

HTTP Code Error Code Description Solution
400 INVALID_REQUEST Invalid JSON format or missing required fields Check request body format
400 INVALID_AMOUNT Amount outside valid range (1,000-10,000,000) Adjust amount
401 UNAUTHORIZED Invalid or missing API key Check API key
403 FORBIDDEN Insufficient permissions Check user role
404 NOT_FOUND Transaction not found Check transaction ID
409 ALREADY_PROCESSED Transaction already paid/expired/cancelled Check transaction status
429 RATE_LIMIT_EXCEEDED Too many requests Wait and retry
500 INTERNAL_ERROR Internal server error Contact support
503 SERVICE_UNAVAILABLE Service temporarily unavailable Retry later

Error Response Format:

{ "success": false, "error": "RATE_LIMIT_EXCEEDED", "message": "Too many requests. Please wait 3 seconds before trying again.", "timestamp": "2024-12-30 12:30:45", "details": { "cooldown_seconds": 3, "endpoint": "create-payment" }}

šŸ“ 9. Changelog

v1.0.0 (Current) - 2024-12-30

Planned Features: