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.
| Field | Source | Example |
|---|---|---|
timestamp | Your system | 2026-03-02T14:23:01.000Z |
correlation_id | Response body | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
http_method | Request | POST |
endpoint | Request | /v1/transactions |
http_status | Response | 200 |
response_status | Response body | declined |
response_code | Response body | 05 |
response_message | Response body | Transaction declined by the issuer. |
elapsed_ms | Your system | 342 |
| Field | Purpose |
|---|---|
idempotency_key | Correlate retries |
transaction_id | Link to gateway record |
processor_code | Processor-specific debugging |
customer_id | Correlate with your user |
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.
| Field | Log this instead |
|---|---|
Full PAN (4111111111111111) | Last four (****1111) |
CVV (123) | Never log |
| API key | ****last4 or omit |
| Bank account number | Last four (****6789) |
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{
"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
}{
"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"
}Once you have structured logs, feed them into a monitoring system to catch issues early.

| Alert | Condition | Severity |
|---|---|---|
| High decline rate | Decline rate > 15% over 15 minutes | Warning |
| Elevated 5xx errors | Any 5xx response from the API | Critical |
| Slow responses | elapsed_ms > 5000 for 5+ requests in 10 minutes | Warning |
| Auth failures | 401 responses spike | Critical |
| Unknown processor code | Processor code not in your mapping | Info |
| Metric | How to compute | Why it matters |
|---|---|---|
| Approval rate | approved / total_transactions | Core business health |
| Decline rate by code | Group declines by response_code | Identify specific card issues |
| API latency (p50, p95, p99) | elapsed_ms percentiles | Performance regression detection |
| Error rate | 5xx_responses / total_requests | Gateway availability |
| Retry rate | Requests with same idempotency_key | Indicates instability |
Every response includes a correlation_id. This value traces the request through Paradise Gateway's internal systems and to the processor.
- Log the
correlation_idalongside your internal order ID. - When investigating an issue, query your logs by
correlation_id. - 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"],
)- Use structured JSON logs — not free-text strings. JSON logs are searchable and parseable by aggregation tools.
- Include
correlation_idon 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.