[Python] "Getting 401 Unauthorized when calling Paytrail API with HMAC authentication – How to...

Discussão em 'Python' iniciado por Stack, Junho 3, 2025.

  1. Stack

    Stack Membro Participativo

    import hmac
    import hashlib
    import json
    import requests
    import uuid
    from datetime import datetime, timedelta
    from typing import Optional
    from .base import BasePaymentProvider


    class PaytrailPaymentProvider(BasePaymentProvider):
    """
    Paytrail payment provider implementation (v2 HMAC).
    """
    def __init__(self, organization, credentials):
    super().__init__(organization, credentials)
    self.merchant = credentials.credentials["MERCHANT_ID"]
    self.secret = credentials.credentials["SECRET_KEY"]
    self.test_mode = credentials.credentials.get("test_mode", True)

    if not self.merchant or not self.secret:
    raise RuntimeError("Missing Paytrail MERCHANT_ID or SECRET_KEY in credentials")

    # Always point at the same host
    self.base_url = "https://services.paytrail.com"

    def _generate_auth_headers(self, method: str, body: str = "") -> dict:
    """
    Build Paytrail v2 HMAC headers:
    1) checkout-account, algorithm, method, nonce, timestamp
    2) signature = HMAC_SHA256(secret, "\n".join(sorted checkout lines + body))
    """
    ts = datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
    nonce = uuid.uuid4().hex

    hdrs = {
    "checkout-account": self.merchant,
    "checkout-algorithm": "sha256",
    "checkout-method": method.upper(),
    "checkout-nonce": nonce,
    "checkout-timestamp": ts,
    }

    # Build the signing string: each header line sorted alphabetically
    lines = [f"{k}:{v}" for k, v in sorted(hdrs.items())]
    lines.append(body)

    signature = hmac.new(
    self.secret.encode("utf-8"),
    "\n".join(lines).encode("utf-8"),
    hashlib.sha256
    ).hexdigest()

    hdrs["checkout-signature"] = signature
    hdrs["Content-Type"] = "application/json"
    return hdrs

    def initiate_payment(
    self,
    amount: int,
    customer_info: dict,
    order_id: Optional[str] = None,
    return_url: Optional[str] = None,
    **kwargs
    ) -> dict:
    """
    Create a Paytrail payment via POST /api/v2/payments.
    """
    # 1) Build the payment payload
    stamp = str(uuid.uuid4())
    payload = {
    "stamp": stamp,
    "reference": stamp,
    "amount": amount,
    "currency": customer_info.get("currency", "EUR"),
    "language": customer_info.get("language", "FI"),
    "items": [{
    "unitPrice": amount,
    "units": 1,
    "vatPercentage": 24,
    "productCode": "default",
    "description": "Payment",
    "deliveryDate": datetime.utcnow().date().isoformat()
    }],
    "customer": {
    "email": customer_info.get("email", "")
    },
    "redirectUrls": {
    "success": return_url or f"{self.base_url}/success",
    "cancel": return_url or f"{self.base_url}/cancel"
    }
    }

    # 2) Compact JSON for signing
    body = json.dumps(payload, separators=(",", ":"), sort_keys=True)

    # 3) Generate HMAC headers
    headers = self._generate_auth_headers("POST", body)

    # 4) Debug log
    print("=== PAYTRAIL DEBUG ===")
    print("URL: ", self.base_url + "/payments")
    print("HEADERS:")
    for k, v in headers.items():
    print(f" {k}: {v}")
    print("BODY:", body)
    print("=======================")

    # 5) Fire the request and handle errors
    try:
    resp = requests.post(
    f"{self.base_url}/payments",
    headers=headers,
    data=body,
    timeout=10
    )
    # If status is 4xx/5xx, print raw body then raise
    if not resp.ok:
    print(">>> PAYTRAIL RESPONSE BODY:", resp.status_code, resp.text)
    resp.raise_for_status()

    # 6) Parse JSON and return
    data = resp.json()
    return {
    "id": data["transactionId"],
    "reference": data["transactionId"],
    "payment_url": data.get("href") or data.get("url"),
    "status": "INITIATED",
    "amount": amount,
    "currency": payload["currency"],
    }

    except requests.exceptions.RequestException as e:
    raise ValueError(f"Payment initiation failed: {str(e)}")


    def get_payment_status(self, reference: str) -> dict:
    """
    Get Paytrail payment status.
    """
    try:
    # Generate headers
    headers = self._generate_auth_headers("GET", "")

    # Make API request
    response = requests.get(
    f"{self.base_url}/payments/{reference}",
    headers=headers
    )
    response.raise_for_status()

    result = response.json()

    # Map Paytrail status to our status
    status_mapping = {
    "new": "INITIATED",
    "ok": "PAID",
    "fail": "FAILED",
    "pending": "PENDING",
    "delayed": "PENDING",

    }

    return {
    "id": reference,
    "status": status_mapping.get(result.get("status", "").lower(), "UNKNOWN"),
    "amount": result.get("amount"),
    "captured": result.get("status", "").lower() == "ok"
    }

    except requests.exceptions.RequestException as e:
    raise ValueError(f"Failed to get payment status: {str(e)}")


    I'm implementing the Paytrail payment provider in Python, and I encounter a 401 Unauthorized error when retrieving payment status via the API.

    What are common reasons for 401 errors in this scenario?

    How can I troubleshoot my HMAC headers and request signature?

    Are there additional headers or steps specific to GET requests in Paytrail's API?

    Continue reading...

Compartilhe esta Página