### Get QR cashier information Source: https://monobank.ua/api-docs/acquiring/methods/qr/post--api--merchant--qr--reset-amount Example request for getting QR cashier information. ```bash curl -X GET \ 'https://api.monobank.ua/api/merchant/qr/details?qrId=XJ_DiM4rTd5V' \ -H 'X-Token: ' ``` -------------------------------- ### Get QR cashier information Source: https://monobank.ua/api-docs/acquiring/methods/qr/get--api--merchant--qr--list Response example for getting QR cashier information. ```json { "shortQrId": "OBJE", "amount": 4200, "ccy": 980, "invoiceId": "4EwIUTA12JIZ" } ``` -------------------------------- ### Node.js Example Source: https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-skills Express server with 3 endpoints: payment creation, status check, and webhook processing with signature verification using native crypto. Run with 'npm install express && node server.js'. Reads token from MONOBANK_TOKEN env variable or http://localhost:3000. ```javascript const express = require('express'); const axios = require('axios'); const crypto = require('crypto'); const fs = require('fs'); const app = express(); const port = process.env.PORT || 3000; const MONOBANK_TOKEN = process.env.MONOBANK_TOKEN || 'YOUR_DEFAULT_TOKEN'; const API_URL = 'https://api.monobank.ua/'; // Load public key for signature verification // IMPORTANT: Replace with your actual public key PEM string const PUBLIC_KEY_PEM = fs.readFileSync('path/to/your/public.pem', 'utf8'); // Or load from env variable // Middleware to parse JSON bodies app.use(express.json()); // Middleware to verify signature const verifySignature = (req, res, buf, encoding) => { const signature = req.headers['x-sign']; const message = buf.toString(encoding); if (!signature) { throw new Error('X-Sign header is missing'); } try { const verify = crypto.createVerify('sha256'); verify.update(message); const isVerified = verify.verify(PUBLIC_KEY_PEM, signature, 'base64'); if (!isVerified) { throw new Error('Invalid signature'); } } catch (error) { console.error('Signature verification failed:', error); throw new Error('Signature verification failed'); } }; // Endpoint to create an invoice app.post('/create-invoice', async (req, res) => { const { amount, ccy = 980, merchantData = '' } = req.body; if (!amount) { return res.status(400).json({ error: 'Amount is required' }); } const headers = { 'Content-Type': 'application/json', 'X-Token': MONOBANK_TOKEN }; const payload = { amount, ccy, merchantData }; try { const response = await axios.post(`${API_URL}acquiring/v1/invoice/create`, payload, { headers }); res.json(response.data); } catch (error) { console.error('Error creating invoice:', error.response ? error.response.data : error.message); res.status(error.response ? error.response.status : 500).json({ error: error.message }); } }); // Endpoint to check payment status app.get('/payment-status/:invoiceId', async (req, res) => { const { invoiceId } = req.params; const headers = { 'Content-Type': 'application/json', 'X-Token': MONOBANK_TOKEN }; try { const response = await axios.get(`${API_URL}acquiring/v1/invoice/status/${invoiceId}`, { headers }); res.json(response.data); } catch (error) { console.error('Error fetching payment status:', error.response ? error.response.data : error.message); res.status(error.response ? error.response.status : 500).json({ error: error.message }); } }); // Endpoint to handle webhooks app.post('/webhook', async (req, res) => { try { // Use the custom middleware to verify signature before parsing JSON verifySignature(req, res, Buffer.from(JSON.stringify(req.body)), 'utf8'); const { status, invoiceId, terminalStatus } = req.body; console.log(`Received webhook for invoice ${invoiceId} with status: ${status} and terminal status: ${terminalStatus}`); // Process terminal statuses: success, failure, hold if (terminalStatus === 'success') { console.log(`Payment successful for invoice ${invoiceId}`); // Add your success processing logic here } else if (terminalStatus === 'failure') { console.log(`Payment failed for invoice ${invoiceId}`); // Add your failure processing logic here } else if (terminalStatus === 'hold') { console.log(`Payment on hold for invoice ${invoiceId}`); // Add your hold processing logic here } res.json({ message: 'Webhook received successfully' }); } catch (error) { console.error('Webhook processing error:', error.message); res.status(400).json({ error: error.message }); } }); app.listen(port, () => { console.log(`Server running on port ${port}`); if (MONOBANK_TOKEN === 'YOUR_DEFAULT_TOKEN') { console.warn('MONOBANK_TOKEN is not set. Using default token. Please set it via environment variable.'); } }); ``` -------------------------------- ### Signature Verification (Go example) Source: https://monobank.ua/api-docs/acquiring/dev/webhooks/get--api--merchant--pubkey Example code in Go demonstrating how to verify webhook signatures. ```go package main import ( "crypto/ecdsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "log" ) func verifySignature(pubKeyBase64, xSignBase64 string, body []byte) bool { // 1. Decode the public key pubKeyBytes, _ := base64.StdEncoding.DecodeString(pubKeyBase64) pubKeyBlock, _ := pem.Decode(pubKeyBytes) pubKeyInterface, _ := x509.ParsePKIXPublicKey(pubKeyBlock.Bytes) pubKey := pubKeyInterface.(*ecdsa.PublicKey) // 2. Decode the signature sign, _ := base64.StdEncoding.DecodeString(xSignBase64) // 3. Hash the body and verify hash := sha256.Sum256(body) return ecdsa.VerifyASN1(pubKey, hash[:], sign) } ``` -------------------------------- ### Signature Verification (Go example) Source: https://monobank.ua/api-docs/acquiring/methods/split/get--api--merchant--pubkey Go example for verifying webhook signatures. ```go package main import ( "crypto/ecdsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "log" ) func verifySignature(pubKeyBase64, xSignBase64 string, body []byte) bool { // 1. Decode the public key pubKeyBytes, _ := base64.StdEncoding.DecodeString(pubKeyBase64) pubKeyBlock, _ := pem.Decode(pubKeyBytes) pubKeyInterface, _ := x509.ParsePKIXPublicKey(pubKeyBlock.Bytes) pubKey := pubKeyInterface.(*ecdsa.PublicKey) // 2. Decode the signature sign, _ := base64.StdEncoding.DecodeString(xSignBase64) // 3. Hash the body and verify hash := sha256.Sum256(body) return ecdsa.VerifyASN1(pubKey, hash[:], sign) } ``` -------------------------------- ### Go Signature Example Source: https://monobank.ua/api-docs/acquiring/methods/monopay/docs--signature-example Example of generating a signature using ECDSA in Go. ```Go package main import ( "crypto/ecdsa" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "os" ) var ( privateKeyPath = "./private.pem" publicKeyPath = "./pubkey.pem" ) func Sign(body []byte) (string, error) { const privateKeyPath = "./private.pem" bytes, err := os.ReadFile(privateKeyPath) if err != nil { return "", err } block, _ := pem.Decode(bytes) var privateKey *ecdsa.PrivateKey if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { if ecdsaKey, ok := key.(*ecdsa.PrivateKey); ok { privateKey = ecdsaKey } } if privateKey == nil { if privateKey, err = x509.ParseECPrivateKey(block.Bytes); err != nil { return "", err } } hash := sha256.Sum256(body) signBytes, err := privateKey.Sign(rand.Reader, hash[:], nil) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(signBytes), nil } ``` -------------------------------- ### Java Signature Example Source: https://monobank.ua/api-docs/acquiring/methods/monopay/docs--signature-example Example of generating a signature using ECDSA in Java. ```Java import java.nio.file.Files; import java.nio.file.Paths; import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; public class MonoPaySign { static byte[] readPem(String path) throws Exception { String pem = new String(Files.readAllBytes(Paths.get(path))); pem = pem.replaceAll("-----BEGIN ([A-Z ]+)-----", "") .replaceAll("-----END ([A-Z ]+)-----", "") .replaceAll("\s", ""); return Base64.getDecoder().decode(pem); } public static String sign(byte[] body, String privateKeyPemPath) throws Exception { byte[] keyBytes = readPem(privateKeyPemPath); // PKCS#8 KeyFactory kf = KeyFactory.getInstance("EC"); PrivateKey privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); Signature sig = Signature.getInstance("SHA256withECDSA"); sig.initSign(privateKey); sig.update(body); byte[] signatureDer = sig.sign(); return Base64.getEncoder().encodeToString(signatureDer); } public static void main(String[] args) throws Exception { byte[] body = "{\"test\":1}".getBytes(); System.out.println(sign(body, "./private_pkcs8.pem")); } } ``` -------------------------------- ### Ruby Signature Example Source: https://monobank.ua/api-docs/acquiring/methods/monopay/docs--signature-example Example of generating a signature using ECDSA in Ruby. ```Ruby require "openssl" require "base64" def sign(body, private_key_path = "./private.pem") pem = File.read(private_key_path) key = OpenSSL::PKey.read(pem) # EC key digest = OpenSSL::Digest::SHA256.digest(body) # DER/ASN.1 ECDSA підпис над digest signature_der = key.dsa_sign_asn1(digest) Base64.strict_encode64(signature_der) end puts sign('{"test":1}') ``` -------------------------------- ### PHP Signature Example Source: https://monobank.ua/api-docs/acquiring/methods/monopay/docs--signature-example Example of generating a signature using ECDSA in PHP. ```Php str: with open(private_key_path, "rb") as f: pem_data = f.read() private_key = serialization.load_pem_private_key( pem_data, password=None, ) digest = hashlib.sha256(body).digest() # Підписуємо вже готовий хеш signature_der = private_key.sign( digest, ec.ECDSA(Prehashed(hashes.SHA256())) ) return base64.b64encode(signature_der).decode("ascii") ``` -------------------------------- ### Node.js Signature Example Source: https://monobank.ua/api-docs/acquiring/methods/monopay/docs--signature-example Example of generating a signature using ECDSA in Node.js. ```NodeJs // CommonJS const fs = require("fs"); const crypto = require("crypto"); // ES Modules // import fs from "fs"; // import crypto from "crypto"; function generateSignature (dataToSign, requestId, privateKeyPath = "./private.pem") { try { const privateKeyPem = fs.readFileSync(privateKeyPath, "utf8"); const privateKeyMatch = privateKeyPem.match(/-----BEGIN PRIVATE KEY-----([\s\S]*?)-----END PRIVATE KEY-----/); if (!privateKeyMatch) { throw new Error("Invalid private key format"); } const keyData = privateKeyMatch[1].replace(/\s/g, ""); const privateKey = crypto.createPrivateKey({ key: Buffer.from(`-----BEGIN PRIVATE KEY-----\n${keyData}\n-----END PRIVATE KEY-----`), format: 'pem', type: 'pkcs8' }); const dataStr = JSON.stringify(dataToSign) + requestId; const dataBytes = Buffer.from(dataStr, 'utf8'); const signature = crypto.sign('sha256', dataBytes, { key: privateKey, dsaEncoding: 'ieee-p1363' }); const derSignature = ecdsaRawToDer(signature); ``` -------------------------------- ### Go Example Source: https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-skills Standard library net/http server with 3 endpoints: payment creation, status check, and webhook processing with signature verification. Run with 'go run main.go'. Reads token from MONOBANK_TOKEN env variable or http://localhost:3000. ```go package main import ( "bytes" "crypto/ecdsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "os" "strings " "github.com/ethereum/go-ethereum/crypto" ) const API_URL = "https://api.monobank.ua/" var MONOBANK_TOKEN = os.Getenv("MONOBANK_TOKEN") // IMPORTANT: Replace with your actual public key PEM string const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----\n...YOUR_PUBLIC_KEY...\n-----END PUBLIC KEY-----` // Structure for creating an invoice request type CreateInvoiceRequest struct { Amount int `json:"amount"` Ccy int `json:"ccy,omitempty"` MerchantData string `json:"merchantData,omitempty"` } // Structure for API responses (simplified) type ApiResponse struct { InvoiceId string `json:"invoiceId,omitempty"` Status string `json:"status,omitempty"` Error string `json:"error,omitempty"` } // Structure for webhook payload type WebhookPayload struct { InvoiceId string `json:"invoiceId,omitempty"` Status string `json:"status,omitempty"` TerminalStatus string `json:"terminalStatus,omitempty"` Amount int `json:"amount,omitempty"` Ccy int `json:"ccy,omitempty"` MerchantData string `json:"merchantData,omitempty"` PaymentId string `json:"paymentId,omitempty"` PaymentTimestamp int64 `json:"paymentTimestamp,omitempty"` } // Helper function to make HTTP requests func makeRequest(method, endpoint string, requestBody interface{}, responseBody interface{}) error { headers := make(http.Header) headers.Set("Content-Type", "application/json") headers.Set("X-Token", MONOBANK_TOKEN) var reqBodyBytes []byte if requestBody != nil { var err error reqBodyBytes, err = json.Marshal(requestBody) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) } } req, err := http.NewRequest(method, API_URL+endpoint, bytes.NewBuffer(reqBodyBytes)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header = headers client := &http.Client{} resp, err := client.Do(req) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() respBodyBytes, err := ioutil.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("API error: %s - %s", resp.Status, string(respBodyBytes)) } if responseBody != nil { if err := json.Unmarshal(respBodyBytes, responseBody); err != nil { return fmt.Errorf("failed to unmarshal response body: %w", err) } } return nil } // Helper function to verify ECDSA signature func verifySignature(signatureBase64 string, message string) (bool, error) { publicKey, err := crypto.LoadX509PublicKeyPEM([]byte(PUBLIC_KEY_PEM)) if err != nil { return false, fmt.Errorf("failed to load public key: %w", err) } ssignature, err := base64.StdEncoding.DecodeString(signatureBase64) if err != nil { return false, fmt.Errorf("failed to decode signature: %w", err) } hash := sha256.Sum256([]byte(message)) isValid := ecdsa.Verify(publicKey.(*ecdsa.PublicKey), hash[:], signature[:len(signature)/2], signature[len(signature)/2:]) return isValid, nil } // Handler for creating an invoice func createInvoiceHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var reqBody CreateInvoiceRequest if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } var resp ApiResponse if err := makeRequest("POST", "acquiring/v1/invoice/create", reqBody, &resp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // Handler for checking payment status func getPaymentStatusHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } invoiceID := strings.TrimPrefix(r.URL.Path, "/payment-status/") if invoiceID == "" { http.Error(w, "Invoice ID is required", http.StatusBadRequest) return } var resp ApiResponse if err := makeRequest("GET", fmt.Sprintf("acquiring/v1/invoice/status/%s", invoiceID), nil, &resp); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } // Handler for webhooks func webhookHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } signatureHeader := r.Header.Get("X-Sign") bodyBytes, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Failed to read request body", http.StatusInternalServerError) return } isValid, err := verifySignature(signatureHeader, string(bodyBytes)) if err != nil { log.Printf("Signature verification error: %v\n", err) http.Error(w, "Internal server error during verification", http.StatusInternalServerError) return } if !isValid { http.Error(w, "Invalid signature", http.StatusBadRequest) return } var payload WebhookPayload if err := json.Unmarshal(bodyBytes, &payload); err != nil { http.Error(w, "Invalid JSON payload", http.StatusBadRequest) return } log.Printf("Received webhook for invoice %s with status: %s and terminal status: %s\n", payload.InvoiceId, payload.Status, payload.TerminalStatus) // Process terminal statuses: success, failure, hold switch payload.TerminalStatus { case "success": log.Printf("Payment successful for invoice %s\n", payload.InvoiceId) // Add your success processing logic here case "failure": log.Printf("Payment failed for invoice %s\n", payload.InvoiceId) // Add your failure processing logic here case "hold": log.Printf("Payment on hold for invoice %s\n", payload.InvoiceId) // Add your hold processing logic here } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message": "Webhook received successfully"}`)) } func main() { if MONOBANK_TOKEN == "" { log.Println("MONOBANK_TOKEN environment variable not set. Using default token.") MONOBANK_TOKEN = "YOUR_DEFAULT_TOKEN" } http.HandleFunc("/create-invoice", createInvoiceHandler) http.HandleFunc("/payment-status/", getPaymentStatusHandler) http.HandleFunc("/webhook", webhookHandler) port := "3000" if MONOBANK_TOKEN != "YOUR_DEFAULT_TOKEN" { port = "5000" } log.Printf("Server starting on port %s\n", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } ``` -------------------------------- ### List all QR cashiers Source: https://monobank.ua/api-docs/acquiring/methods/qr/post--api--merchant--qr--reset-amount Example request for listing all QR cashiers. ```bash curl -X GET \ 'https://api.monobank.ua/api/merchant/qr/list' \ -H 'X-Token: ' ``` -------------------------------- ### Get Invoice Status Request Example Source: https://monobank.ua/api-docs/acquiring/methods/qr/get--api--merchant--invoice--status Example of how to request the status of an invoice. ```http GET /api/merchant/invoice/status?invoiceId=p2_9ZgpZVsl3 Host: api.monobank.ua X-Token: YOUR_MERCHANT_TOKEN ``` -------------------------------- ### Java Example Source: https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-skills JDK HttpServer with 3 endpoints: payment creation, status check, and webhook processing with signature verification. Run with 'javac MonobankServer.java && java MonobankServer'. Reads token from MONOBANK_TOKEN env variable or http://localhost:3000. ```java import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.Map; import java.util.Scanner; import java.util.stream.Collectors; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.jce.spec.ECPublicKeySpec; import org.bouncycastle.math.ec.ECPoint; import org.bouncycastle.jce.spec.ECParameterSpec; import org.bouncycastle.jce.ec.CustomNamedCurves; public class MonobankServer { private static final String API_URL = "https://api.monobank.ua/"; private static final String MONOBANK_TOKEN = System.getenv("MONOBANK_TOKEN") != null ? System.getenv("MONOBANK_TOKEN") : "YOUR_DEFAULT_TOKEN"; // IMPORTANT: Replace with your actual public key PEM string private static final String PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\n...YOUR_PUBLIC_KEY...\n-----END PUBLIC KEY-----"; private static final HttpClient httpClient = HttpClient.newHttpClient(); public static void main(String[] args) throws IOException { int port = MONOBANK_TOKEN.equals("YOUR_DEFAULT_TOKEN") ? 3000 : 5000; HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); server.createContext("/create-invoice", new CreateInvoiceHandler()); server.createContext("/payment-status/", new GetPaymentStatusHandler()); server.createContext("/webhook", new WebhookHandler()); server.setExecutor(null); // creates a default executor server.start(); System.out.println("Server started on port " + port); } // Handler for creating an invoice static class CreateInvoiceHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { String requestBody = new Scanner(exchange.getRequestBody()).useDelimiter("\Z").next(); Map requestData = new com.fasterxml.jackson.databind.ObjectMapper().readValue(requestBody, Map.class); if (!requestData.containsKey("amount")) { sendResponse(exchange, 400, "{\"error\": \"Amount is required\"}"); return; } HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(API_URL + "acquiring/v1/invoice/create")) .header("Content-Type", "application/json") .header("X-Token", MONOBANK_TOKEN) .POST(HttpRequest.BodyPublishers.ofString(new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(requestData))) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); sendResponse(exchange, response.statusCode(), response.body()); } catch (Exception e) { sendResponse(exchange, 500, "{\"error\": \"" + e.getMessage().replace("\"", "\\\"") + "\"}"); } } } // Handler for checking payment status static class GetPaymentStatusHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { if (!exchange.getRequestMethod().equalsIgnoreCase("GET")) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { String invoiceId = exchange.getRequestURI().getPath().replace("/payment-status/", ""); if (invoiceId.isEmpty()) { sendResponse(exchange, 400, "{\"error\": \"Invoice ID is required\"}"); return; } HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(API_URL + "acquiring/v1/invoice/status/" + invoiceId)) .header("Content-Type", "application/json") .header("X-Token", MONOBANK_TOKEN) .GET() .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); sendResponse(exchange, response.statusCode(), response.body()); } catch (Exception e) { sendResponse(exchange, 500, "{\"error\": \"" + e.getMessage().replace("\"", "\\\"") + "\"}"); } } } // Handler for webhooks static class WebhookHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { String signatureBase64 = exchange.getRequestHeaders().getFirst("X-Sign"); String requestBody = new Scanner(exchange.getRequestBody()).useDelimiter("\Z").next(); if (signatureBase64 == null || signatureBase64.isEmpty()) { sendResponse(exchange, 400, "{\"error\": \"X-Sign header is missing\"}"); return; } if (!verifySignature(signatureBase64, requestBody)) { sendResponse(exchange, 400, "{\"error\": \"Invalid signature\"}"); return; } Map payload = new com.fasterxml.jackson.databind.ObjectMapper().readValue(requestBody, Map.class); String invoiceId = (String) payload.get("invoiceId"); String terminalStatus = (String) payload.get("terminalStatus"); System.out.println("Received webhook for invoice " + invoiceId + " with terminal status: " + terminalStatus); // Process terminal statuses: success, failure, hold switch (terminalStatus) { case "success": System.out.println("Payment successful for invoice " + invoiceId); // Add your success processing logic here break; case "failure": System.out.println("Payment failed for invoice " + invoiceId); // Add your failure processing logic here break; case "hold": System.out.println("Payment on hold for invoice " + invoiceId); // Add your hold processing logic here break; } sendResponse(exchange, 200, "{\"message\": \"Webhook received successfully\"}"); } catch (Exception e) { e.printStackTrace(); sendResponse(exchange, 500, "{\"error\": \"" + e.getMessage().replace("\"", "\\\"") + "\"}"); } } } private static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException { exchange.sendResponseHeaders(statusCode, response.getBytes(StandardCharsets.UTF_8).length); OutputStream os = exchange.getResponseBody(); os.write(response.getBytes(StandardCharsets.UTF_8)); os.close(); } // Helper method to verify ECDSA signature using Bouncy Castle private static boolean verifySignature(String signatureBase64, String message) { try { // Register Bouncy Castle provider if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(new BouncyCastleProvider()); } byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64); byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); // Parse PEM public key PEMParser pemParser = new PEMParser(new java.io.StringReader(PUBLIC_KEY_PEM)); Object pemObject = pemParser.readObject(); SubjectPublicKeyInfo publicKeyInfo; if (pemObject instanceof SubjectPublicKeyInfo) { publicKeyInfo = (SubjectPublicKeyInfo) pemObject; } else { throw new IllegalArgumentException("PEM object is not a SubjectPublicKeyInfo"); } PublicKey publicKey = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME).generatePublic(new X509EncodedKeySpec(publicKeyInfo.getEncoded())); Signature signature = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME); signature.initVerify(publicKey); signature.update(messageBytes); return signature.verify(signatureBytes); } catch (Exception e) { e.printStackTrace(); return false; } } } ``` -------------------------------- ### Get Invoice Status Source: https://monobank.ua/api-docs/acquiring/methods/in-app/get--api--merchant--invoice--status Example of how to check the status of an invoice using the GET /api/merchant/invoice/status endpoint. ```bash curl -X GET \ 'https://api.monobank.ua/api/merchant/invoice/status?invoiceId=p2_9ZgpZVsl3' \ -H 'X-Token: ' ``` -------------------------------- ### Response 200 Example Source: https://monobank.ua/api-docs/acquiring/extras/statement/get--api--merchant--statement Example of a successful response for the GET /api/merchant/statement endpoint, showing transaction details. ```json { "list": [ { "invoiceId": "2205175v4MfatvmUL2oR", "status": "success", "maskedPan": "444403******1902", "date": "2023-01-01T12:00:00Z", "amount": 4200, "ccy": 980, "profitAmount": 4100, "reference": "84d0070ee4e44667b31371d8f8813947", "destination": "Покупка щастя", "approvalCode": "662476", "rrn": "060189181768", "paymentScheme": "full", "shortQrId": null, "cancelList": [] } ] } ``` -------------------------------- ### C# Example Source: https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-skills ASP.NET Minimal API with 3 endpoints: payment creation, status check, and webhook processing with signature verification. Run with 'dotnet run'. Reads token from MONOBANK_TOKEN env variable or http://localhost:3000. ```csharp using Microsoft.AspNetCore.Mvc; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.IO; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configure HttpClient builder.Services.AddHttpClient(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); var MONOBANK_TOKEN = Environment.GetEnvironmentVariable("MONOBANK_TOKEN") ?? "YOUR_DEFAULT_TOKEN"; var API_URL = "https://api.monobank.ua/"; // IMPORTANT: Load your public key PEM string here // You can get this from your Monobank merchant account or API settings var PUBLIC_KEY_PEM = File.ReadAllText("path/to/your/public.pem"); // Or load from env variable var publicKey = LoadPublicKeyFromPem(PUBLIC_KEY_PEM); // Endpoint to create an invoice app.MapPost("/create-invoice", async (HttpContext context, [FromBody] CreateInvoiceRequest request) => { if (string.IsNullOrEmpty(request.Amount)) { return Results.BadRequest(new { error = "Amount is required" }); } var client = context.RequestServices.GetRequiredService(); var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, API_URL + "acquiring/v1/invoice/create"); httpRequestMessage.Headers.Add("X-Token", MONOBANK_TOKEN); httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var payload = new { amount = int.Parse(request.Amount), ccy = request.Ccy ?? 980, merchantData = request.MerchantData ?? "" }; httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); try { var response = await client.SendAsync(httpRequestMessage); var responseContent = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { return Results.StatusCode((int)response.StatusCode, new { error = responseContent }); } return Results.Content(responseContent, "application/json"); } catch (Exception ex) { return Results.StatusCode(500, new { error = ex.Message }); } }) .WithName("CreateInvoice") .WithDescription("Creates a new invoice for payment."); // Endpoint to check payment status app.MapGet("/payment-status/{invoiceId}", async (HttpContext context, string invoiceId) => { var client = context.RequestServices.GetRequiredService(); var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, API_URL + $"acquiring/v1/invoice/status/{invoiceId}"); httpRequestMessage.Headers.Add("X-Token", MONOBANK_TOKEN); httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); try { var response = await client.SendAsync(httpRequestMessage); var responseContent = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { return Results.StatusCode((int)response.StatusCode, new { error = responseContent }); } return Results.Content(responseContent, "application/json"); } catch (Exception ex) { return Results.StatusCode(500, new { error = ex.Message }); } }) .WithName("GetPaymentStatus") .WithDescription("Retrieves the status of a payment."); // Endpoint to handle webhooks app.MapPost("/webhook", async (HttpContext context) => { var signature = context.Request.Headers["X-Sign"].FirstOrDefault(); string requestBody; using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, true, 1024, false)) { requestBody = await reader.ReadToEndAsync(); } if (string.IsNullOrEmpty(signature)) { return Results.BadRequest(new { error = "X-Sign header is missing" }); } try { if (!VerifySignature(signature, requestBody, publicKey)) { return Results.BadRequest(new { error = "Invalid signature" }); } var payload = JsonSerializer.Deserialize(requestBody); Console.WriteLine($"Received webhook for invoice {payload.InvoiceId} with status: {payload.Status} and terminal status: {payload.TerminalStatus}"); // Process terminal statuses: success, failure, hold switch (payload.TerminalStatus) { case "success": Console.WriteLine($"Payment successful for invoice {payload.InvoiceId}"); // Add your success processing logic here break; case "failure": Console.WriteLine($"Payment failed for invoice {payload.InvoiceId}"); // Add your failure processing logic here break; case "hold": Console.WriteLine($"Payment on hold for invoice {payload.InvoiceId}"); // Add your hold processing logic here break; } return Results.Ok(new { message = "Webhook received successfully" }); } catch (JsonException ex) { return Results.BadRequest(new { error = "Invalid JSON payload", details = ex.Message }); } catch (Exception ex) { Console.Error.WriteLine($"Webhook processing error: {ex.Message}"); return Results.StatusCode(500, new { error = "Internal server error" }); } }) .WithName("Webhook") .WithDescription("Handles incoming webhooks from Monobank."); app.Run(); // Helper classes (define these in your project) public class CreateInvoiceRequest { public string Amount { get; set; } public int? Ccy { get; set; } public string MerchantData { get; set; } } public class WebhookPayload { public string InvoiceId { get; set; } public string Status { get; set; } public string TerminalStatus { get; set; } public int Amount { get; set; } public int Ccy { get; set; } public string MerchantData { get; set; } public string PaymentId { get; set; } public long PaymentTimestamp { get; set; } } // Helper method to load public key from PEM format public static AsymmetricKeyParameter LoadPublicKeyFromPem(string pem) { var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(new StringReader(pem)); var publicKeyObject = pemReader.ReadObject(); if (publicKeyObject is AsymmetricKeyParameter publicKeyParam) { return publicKeyParam; } else if (publicKeyObject is Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters rsaKey) { return rsaKey; } else if (publicKeyObject is Org.BouncyCastle.Crypto.Parameters.ECPublicKeyParameters ecKey) { return ecKey; } throw new Exception("Unknown key type in PEM file."); } // Helper method to verify ECDSA signature public static bool VerifySignature(string signatureBase64, string message, AsymmetricKeyParameter publicKey) { try { var signatureBytes = Convert.FromBase64String(signatureBase64); var messageBytes = Encoding.UTF8.GetBytes(message); var hashBytes = SHA256.Create().ComputeHash(messageBytes); var signer = new Org.BouncyCastle.Crypto.Signers.ECDsaSigner(); signer.Init(false, publicKey); return signer.VerifySignature(hashBytes, signatureBytes); } catch (Exception ex) { Console.Error.WriteLine($"Signature verification failed: {ex.Message}"); return false; } } ``` -------------------------------- ### GET /api/merchant/wallet Response 200 Source: https://monobank.ua/api-docs/acquiring/extras/tokens/get--api--merchant--wallet Example response for listing tokenized cards in a wallet. ```json { "wallet": [ { "cardToken": "67XZtXdR4NpKU3", "maskedPan": "424242******4242", "country": "804" } ] } ``` -------------------------------- ### PHP Example Source: https://monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-skills Built-in server with 3 endpoints: payment creation, status check, and webhook processing with signature verification. Run with 'php -S localhost:3000 server.php'. Reads token from MONOBANK_TOKEN env variable or http://localhost:3000. ```php = 300) { throw new Exception('API Error: HTTP ' . $httpCode . ' - ' . $response); } return json_decode($response, true); } // Helper function to verify ECDSA signature function verifySignature($signature, $message, $publicKeyPem) { // PHP's openssl_verify requires the key to be loaded $key = openssl_pkey_get_public($publicKeyPem); if (!$key) { throw new Exception('Failed to load public key: ' . openssl_error_string()); } // The message needs to be hashed before verification $hash = hash('sha256', $message, true); // Signature is usually base64 encoded $signatureDecoded = base64_decode($signature); if ($signatureDecoded === false) { throw new Exception('Failed to decode signature'); } // openssl_verify expects the signature in raw binary format $result = openssl_verify($message, $signatureDecoded, $key, OPENSSL_ALGO_SHA256); openssl_free_key($key); return $result === 1; } // Routing $requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $requestMethod = $_SERVER['REQUEST_METHOD']; if ($requestUri === '/create-invoice' && $requestMethod === 'POST') { try { $data = json_decode(file_get_contents('php://input'), true); if (!isset($data['amount'])) { throw new Exception('Amount is required', 400); } $amount = $data['amount']; $ccy = $data['ccy'] ?? 980; // Default to UAH $merchantData = $data['merchantData'] ?? ''; $payload = [ 'amount' => $amount, 'ccy' => $ccy, 'merchantData' => $merchantData ]; $response = makeRequest('POST', 'acquiring/v1/invoice/create', $payload); echo json_encode($response); } catch (Exception $e) { http_response_code($e->getCode() ?: 500); echo json_encode(['error' => $e->getMessage()]); } } elseif (preg_match('/\/payment-status\/([a-zA-Z0-9-]+)/', $requestUri, $matches) && $requestMethod === 'GET') { try { $invoiceId = $matches[1]; $response = makeRequest('GET', 'acquiring/v1/invoice/status/' . $invoiceId); echo json_encode($response); } catch (Exception $e) { http_response_code($e->getCode() ?: 500); echo json_encode(['error' => $e->getMessage()]); } } elseif ($requestUri === '/webhook' && $requestMethod === 'POST') { try { $signature = $_SERVER['HTTP_X_SIGN'] ?? ''; $message = file_get_contents('php://input'); if (empty($signature)) { throw new Exception('X-Sign header is missing', 400); } if (!verifySignature($signature, $message, $PUBLIC_KEY_PEM)) { throw new Exception('Invalid signature', 400); } $data = json_decode($message, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON payload', 400); } $status = $data['status'] ?? 'unknown'; $invoiceId = $data['invoiceId'] ?? 'N/A'; $terminalStatus = $data['terminalStatus'] ?? 'unknown'; echo json_encode(['message' => 'Webhook received successfully']); // Process terminal statuses: success, failure, hold switch ($terminalStatus) { case 'success': error_log("Payment successful for invoice {$invoiceId}"); // Add your success processing logic here break; case 'failure': error_log("Payment failed for invoice {$invoiceId}"); // Add your failure processing logic here break; case 'hold': error_log("Payment on hold for invoice {$invoiceId}"); // Add your hold processing logic here break; } } catch (Exception $e) { http_response_code($e->getCode() ?: 500); echo json_encode(['error' => $e->getMessage()]); } } else { http_response_code(404); echo json_encode(['error' => 'Not Found']); } ?> ```