#!/usr/bin/env bash # AntPay hosted checkout with cURL. Needs bash, curl and sha256sum (coreutils). Run it on your SERVER. # Secrets are read from the environment and never appear on a command line (so not in `ps` or shell history). # [setup] set -euo pipefail API_BASE="${ANTPAY_API_BASE:-https://api.antpay.network}" : "${ANTPAY_API_KEY:?set ANTPAY_API_KEY}" # your payment key SIGNING_SECRET="${ANTPAY_SIGNING_SECRET:-}" # only if request signing is on for your account POSTBACK_SECRET="${ANTPAY_POSTBACK_SECRET:-}" # issued with your account, 64 characters # HMAC-SHA256 in hex, key up to 64 characters, message on standard input. Pure bash + sha256sum: the key is handled # by shell built-ins only, so it is never an argument of another program. antpay_hmac_hex() { # antpay_hmac_hex KEY < message local key="$1" ipad="" opad="" i c inner for ((i = 0; i < 64; i++)); do if ((i < ${#key})); then printf -v c '%d' "'${key:i:1}"; else c=0; fi printf -v ipad '%s\\x%02x' "$ipad" $((c ^ 0x36)) printf -v opad '%s\\x%02x' "$opad" $((c ^ 0x5c)) done inner=$({ printf "$ipad"; cat; } | sha256sum | cut -d' ' -f1) { printf "$opad"; printf "$(printf '%s' "$inner" | sed 's/../\\x&/g')"; } | sha256sum | cut -d' ' -f1 } # Percent-encode a value for use inside a URL path. antpay_urlencode() { local s="$1" out="" i c for ((i = 0; i < ${#s}; i++)); do c="${s:i:1}" case "$c" in [A-Za-z0-9._~-]) out+="$c" ;; *) printf -v c '%%%02X' "'$c"; out+="$c" ;; esac done printf '%s' "$out" } # Wait at least 80 seconds for an answer. The API can take up to about 75 s on a create; giving up earlier and # sending the create again while the first is still running is how one order ends up with two checkouts. ANTPAY_TIMEOUT_S=80 # [/setup] # [sign] # Request signing (only if it is switched on for your account). Prints the two headers for one request. # canonical = "{timestamp}.{METHOD}.{path with query}.{body exactly as sent}" antpay_sign() { # antpay_sign METHOD PATH BODY [TIMESTAMP] local method path="$2" body="${3:-}" ts="${4:-$(date +%s)}" method=$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]') printf 'X-Timestamp: %s\nX-Signature: %s\n' "$ts" "$(printf '%s' "${ts}.${method}.${path}.${body}" | antpay_hmac_hex "$SIGNING_SECRET")" } # [/sign] # [call] antpay_call() { # antpay_call METHOD PATH [BODY] prints the answer, then the HTTP status on its own line local method="$1" path="$2" body="${3:-}" h local config="url = \"${API_BASE}${path}\""$'\n'"request = \"${method}\""$'\n'"max-time = ${ANTPAY_TIMEOUT_S}"$'\n'"header = \"X-Api-Key: ${ANTPAY_API_KEY}\"" if [ -n "$body" ]; then config+=$'\n''header = "Content-Type: application/json"'; fi if [ -n "$SIGNING_SECRET" ]; then while IFS= read -r h; do config+=$'\n'"header = \"$h\""; done < <(antpay_sign "$method" "$path" "$body") fi # the key and the signature go to curl through a config on standard input, the body through a second descriptor if [ -n "$body" ]; then curl -sS -K - --data-binary @/dev/fd/3 -w '\n%{http_code}\n' 3< <(printf '%s' "$body") <<< "$config" else curl -sS -K - -w '\n%{http_code}\n' <<< "$config"; fi } # [/call] # [create] # Step 1: create the checkout. Put the request in a file, so the JSON is exactly what you wrote, for example # {"reference":"ORD-10021","amount":1500,"paymentMethod":"qr","paymentType":"onetime","returnUrl":"https://shop.example.com/orders/ORD-10021"} # The answer carries checkoutUrl: send the customer there. antpay_create_checkout() { # antpay_create_checkout FILE_WITH_JSON_BODY antpay_call POST /api/v1/checkouts "$(cat "$1")" } # [/create] # [status] # Ask for the payment by YOUR reference, or by checkoutId. A 404 whose "error" is "Transaction not found" means there # is no such payment. Any other 404 means the address itself was refused: check the reference, do not create again. antpay_get_by_reference() { antpay_call GET "/api/v1/transactions/$(antpay_urlencode "$1")"; } antpay_get_checkout() { antpay_call GET "/api/v1/checkouts/$(antpay_urlencode "$1")"; } # [/status] # [verify] # Check a postback by hand: the header must equal "sha256=" + HMAC-SHA256(raw body, your postback secret). antpay_postback_signature() { # antpay_postback_signature FILE_WITH_RAW_BODY printf 'sha256=%s\n' "$(antpay_hmac_hex "$POSTBACK_SECRET" < "$1")" } # [/verify] # [postback] # Test YOUR postback URL before go-live: send it a postback signed with your own postback secret. antpay_send_test_postback() { # antpay_send_test_postback YOUR_URL FILE_WITH_JSON_BODY curl -sS -X POST "$1" -H 'Content-Type: application/json' \ -H "X-Mint-Signature: $(antpay_postback_signature "$2")" --data-binary "@$2" -w '\n%{http_code}\n' } # [/postback]