Skip to content
Last updated

Tutorial: Log and monitor API errors

Why logging matters

Payment errors are inevitable — network blips, expired cards, processor downtime. What separates a resilient integration from a brittle one is how quickly you detect and respond. Structured logs give you:

  • Root-cause analysis — trace a failed payment through every hop using correlation_id.
  • Trend detection — spot rising decline rates before customers complain.
  • Compliance evidence — auditable records for PCI-DSS and dispute resolution.

What to log

Required fields

FieldSourceExample
timestampYour system2026-03-02T14:23:01.000Z
correlation_idResponse bodya1b2c3d4-e5f6-7890-abcd-ef1234567890
http_methodRequestPOST
endpointRequest/v1/transactions
http_statusResponse200
response_statusResponse bodydeclined
response_codeResponse body05
response_messageResponse bodyTransaction declined by the issuer.
elapsed_msYour system342

Useful additions

FieldPurpose
idempotency_keyCorrelate retries
transaction_idLink to gateway record
processor_codeProcessor-specific debugging
customer_idCorrelate with your user

What NOT to log

PCI compliance

Never log full card numbers (PAN), CVV, bank account numbers, or API keys. Violations can result in PCI-DSS non-compliance and significant fines.

FieldLog this instead
Full PAN (4111111111111111)Last four (****1111)
CVV (123)Never log
API key****last4 or omit
Bank account numberLast four (****6789)

Structured logging implementation

import json, logging, time, os, requests

logger = logging.getLogger("paradise")
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(handler)
logger.setLevel(logging.INFO)

API_KEY = os.environ["PARADISE_API_KEY"]
BASE = os.environ.get("PARADISE_BASE_URL", "https://api.dev.paradisegateway.net")

def logged_request(method: str, path: str, payload: dict | None = None) -> dict:
    url = f"{BASE}{path}"
    headers = {"Content-Type": "application/json", "APIKEY": API_KEY}

    start = time.monotonic()
    try:
        resp = requests.request(method, url, headers=headers, json=payload, timeout=30)
        body = resp.json()
        elapsed = int((time.monotonic() - start) * 1000)

        log_entry = {
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()),
            "level": "error" if resp.status_code >= 400 else "info",
            "http_method": method,
            "endpoint": path,
            "http_status": resp.status_code,
            "response_status": body.get("response_status"),
            "response_code": body.get("response_code"),
            "correlation_id": body.get("correlation_id"),
            "transaction_id": body.get("id"),
            "elapsed_ms": elapsed,
        }

        if resp.status_code >= 400 or body.get("response_status") in ("declined", "error"):
            log_entry["response_message"] = body.get("response_message")
            logger.error(json.dumps(log_entry))
        else:
            logger.info(json.dumps(log_entry))

        return body
    except Exception as e:
        elapsed = int((time.monotonic() - start) * 1000)
        logger.error(json.dumps({
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()),
            "level": "error",
            "http_method": method,
            "endpoint": path,
            "error": str(e),
            "elapsed_ms": elapsed,
        }))
        raise

Example log output

Approved transaction

{
  "timestamp": "2026-03-02T14:23:01.000Z",
  "level": "info",
  "http_method": "POST",
  "endpoint": "/v1/transactions",
  "http_status": 200,
  "response_status": "approved",
  "response_code": "00",
  "correlation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "transaction_id": "TRANSACTION-01KEW32V6YNV11T33XGDR7TGWC",
  "elapsed_ms": 342
}

Declined transaction

{
  "timestamp": "2026-03-02T14:24:15.000Z",
  "level": "error",
  "http_method": "POST",
  "endpoint": "/v1/transactions",
  "http_status": 200,
  "response_status": "declined",
  "response_code": "51",
  "correlation_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "transaction_id": "TRANSACTION-01KEW33R7ZOW22U44YHES8UHXD",
  "elapsed_ms": 287,
  "response_message": "Insufficient funds"
}

Monitoring and alerting

Once you have structured logs, feed them into a monitoring system to catch issues early.

image

AlertConditionSeverity
High decline rateDecline rate > 15% over 15 minutesWarning
Elevated 5xx errorsAny 5xx response from the APICritical
Slow responseselapsed_ms > 5000 for 5+ requests in 10 minutesWarning
Auth failures401 responses spikeCritical
Unknown processor codeProcessor code not in your mappingInfo

Key metrics to track

MetricHow to computeWhy it matters
Approval rateapproved / total_transactionsCore business health
Decline rate by codeGroup declines by response_codeIdentify specific card issues
API latency (p50, p95, p99)elapsed_ms percentilesPerformance regression detection
Error rate5xx_responses / total_requestsGateway availability
Retry rateRequests with same idempotency_keyIndicates instability

Using correlation_id for troubleshooting

Every response includes a correlation_id. This value traces the request through Paradise Gateway's internal systems and to the processor.

  1. Log the correlation_id alongside your internal order ID.
  2. When investigating an issue, query your logs by correlation_id.
  3. When contacting Paradise Gateway support, provide the correlation_id — it lets the team trace the request end-to-end.
log.info(
    "Payment for order %s: correlation_id=%s, status=%s",
    order_id,
    response["correlation_id"],
    response["response_status"],
)

Best practices

  • Use structured JSON logs — not free-text strings. JSON logs are searchable and parseable by aggregation tools.
  • Include correlation_id on every log line — it is your primary tracing key.
  • Redact sensitive data before logging — mask PAN, CVV, and API keys.
  • Set log retention — keep payment logs for at least 12 months for dispute resolution.
  • Separate payment logs — use a dedicated logger/channel for payment-related events to reduce noise.

Next steps