Webhooks

Protocol: HTTPS POST Format: JSON Auth: HMAC-SHA256 signature

Webhooks deliver real-time payment event notifications to your application. Rather than polling the API for status updates, configure a webhook endpoint to receive a push notification whenever a payment status changes β€” submitted, settled, returned, or held for compliance review.

Recommended for ACH and SWIFT Webhooks are especially important for batch and correspondent rails (ACH, SWIFT) where settlement may occur hours or days after the payment is initiated. Subscribe to payment.settled and payment.returned events to update your core banking records when ACH settlement confirmations and return items arrive.

How Webhooks Work

  1. You register a webhook endpoint URL in the PayPlus Admin Console or via the Webhooks API.
  2. When a payment event occurs, PayPlus sends an HTTPS POST request to your endpoint with the event payload in the request body.
  3. Your endpoint responds with HTTP 200 to acknowledge receipt. Any other response code (or no response within 30 seconds) triggers the retry sequence.
  4. PayPlus logs all webhook delivery attempts β€” successful and failed β€” accessible at Administration > API > Webhook Logs.
Idempotent Webhook Handlers Required Because of retry logic, your endpoint may receive the same event more than once. Use the eventId field to deduplicate events before processing β€” store event IDs you've already handled and skip duplicates.

Webhook Registration

Register via Admin Console

Navigate to Administration > API > Webhooks > Add Endpoint. Enter the endpoint URL, select the event types, and select Save. PayPlus generates and displays the signing secret β€” copy it before leaving this page. It’s shown only once.

Register via API

POST /v2/webhooks

Required scope: webhooks:manage

POST /v2/webhooks HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Content-Type: application/json

{
  "url": "https://core.yourbank.internal/payplus/events",
  "events": [
    "payment.settled",
    "payment.returned",
    "compliance.hold",
    "compliance.released"
  ],
  "description": "Core banking settlement integration",
  "enabled": true
}

201 Created

{
  "data": {
    "webhookId": "wh_a3b9c1d7e2f4",
    "url": "https://core.yourbank.internal/payplus/events",
    "events": ["payment.settled", "payment.returned", "compliance.hold", "compliance.released"],
    "signingSecret": "whsec_7f3b2c9a1d4e6f8a0b2c4d6e8f0a2b4c",
    "enabled": true,
    "createdAt": "2026-03-15T09:00:00Z"
  }
}
Store the Signing Secret The signingSecret is returned only once in this response. Store it in your application's secrets management system immediately. If you lose the signing secret, you must delete and re-create the webhook registration.

Manage Webhooks

GET /v2/webhooks

List all registered webhook endpoints for this API client.

GET /v2/webhooks/{webhookId}

Get a specific webhook registration. Does not return the signing secret.

DELETE /v2/webhooks/{webhookId}

Delete a webhook endpoint. PayPlus will stop sending events to this URL immediately.

Signature Verification

Every webhook request includes an X-PayPlus-Signature header containing an HMAC-SHA256 signature. Always verify this signature before processing the event β€” it proves the request came from PayPlus and that the payload has not been tampered with in transit.

Verification Algorithm

# Python example
import hmac
import hashlib

def verify_signature(payload_body: bytes, signature_header: str, signing_secret: str) -> bool:
    # signature_header format: "t=1742038800,v1=abc123..."
    parts = dict(part.split("=", 1) for part in signature_header.split(","))
    timestamp = parts["t"]
    received_sig = parts["v1"]

    # Construct the signed payload: timestamp + "." + raw body
    signed_payload = f"{timestamp}.".encode() + payload_body

    # Compute HMAC-SHA256
    expected_sig = hmac.new(
        signing_secret.encode(),
        signed_payload,
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison to prevent timing attacks
    if not hmac.compare_digest(expected_sig, received_sig):
        return False  # Signature mismatch β€” reject the request

    # Optional: reject events older than 5 minutes (replay protection)
    import time
    if abs(time.time() - int(timestamp)) > 300:
        return False

    return True

Event Reference

Event TypeDescriptionApplies To
payment.validatedPayment passed format validation and entered the workflowAll rails
payment.approvedPayment approved (by workflow rule or human approver); queued for submissionAll rails
payment.submittedPayment submitted to the payment networkAll rails
payment.settledPayment settled β€” funds have moved. For ACH: settlement date passed and FedACH confirmation received. For Fedwire/RTP/FedNow: real-time settlement confirmation received.All rails
payment.returnedPayment returned by the receiving bank after settlement. Includes return code and reason.ACH, RTP, FedNow
payment.rejectedPayment rejected by PayPlus validation, compliance review, approval, or the payment networkAll rails
compliance.holdPayment placed on OFAC compliance hold pending Compliance Officer reviewAll rails
compliance.releasedCompliance hold released β€” payment cleared to continue processingAll rails
compliance.rejectedPayment rejected by Compliance Officer from the hold queueAll rails
wire.recall.resolvedWire recall request has been accepted (funds returned) or rejected by the beneficiary bankFedwire, SWIFT
rfp.fulfilledRequest for Payment has been fulfilled β€” the payer approved the RfP and payment settledRTP, FedNow
rfp.declinedPayer declined the Request for PaymentRTP, FedNow
batch.completedACH batch processing completed β€” includes counts of valid, invalid, submitted, and returned recordsACH

Retry Logic

If your endpoint does not return HTTP 200 within 30 seconds, PayPlus retries the delivery using an exponential backoff schedule:

AttemptDelay After Previous Attempt
1 (initial)β€”
21 minute
35 minutes
430 minutes
52 hours
6 (final)6 hours

After 6 failed attempts, PayPlus marks the delivery as failed and sends an alert to the System Administrator. Events are retained for 72 hours for manual retry via the Admin Console.

Payload Examples

payment.settled β€” ACH

{
  "eventId": "evt_7f3c1d9e2a4b",
  "eventType": "payment.settled",
  "timestamp": "2026-03-17T12:00:00Z",
  "data": {
    "paymentId": "pmt_ach_a3k9f2m8x7p4r1q0",
    "rail": "ach",
    "status": "settled",
    "amount": 350000,
    "effectiveDate": "2026-03-17",
    "traceNumber": "021000021000001",
    "referenceId": "VENDOR-PMT-20260315-001",
    "settledAt": "2026-03-17T12:00:00Z"
  }
}

payment.returned β€” ACH R01

{
  "eventId": "evt_9a1c3e5f7b2d",
  "eventType": "payment.returned",
  "timestamp": "2026-03-18T08:30:00Z",
  "data": {
    "paymentId": "pmt_ach_a3k9f2m8x7p4r1q0",
    "rail": "ach",
    "status": "returned",
    "returnCode": "R01",
    "returnReason": "Insufficient Funds",
    "returnSettlementDate": "2026-03-18",
    "representmentEligible": true,
    "representmentDeadline": "2026-09-14",
    "referenceId": "VENDOR-PMT-20260315-001"
  }
}

compliance.hold β€” OFAC Screening

{
  "eventId": "evt_b2d4f6a8c0e1",
  "eventType": "compliance.hold",
  "timestamp": "2026-03-15T14:35:12Z",
  "data": {
    "paymentId": "pmt_wire_f3a1b9c7d2e4",
    "rail": "swift",
    "holdReason": "OFAC_POSSIBLE_MATCH",
    "matchedField": "beneficiary.name",
    "matchScore": 88,
    "referenceId": "CLOSING-2026-03-15-001",
    "heldAt": "2026-03-15T14:35:12Z"
  }
}

payment.settled β€” FedNow (real-time)

{
  "eventId": "evt_d3f5a7c9e1b2",
  "eventType": "payment.settled",
  "timestamp": "2026-03-15T14:30:07Z",
  "data": {
    "paymentId": "pmt_inst_b9c3d1f7e2a4",
    "rail": "fednow",
    "status": "settled",
    "amount": 50000,
    "uetr": "4a7f2b9c-3d1e-5f6a-8b9c-0d1e2f3a4b5c",
    "settlementTimestamp": "2026-03-15T14:30:07Z",
    "processingTimeMs": 5823,
    "referenceId": "RENT-MAR2026-JD"
  }
}
← Wire Transfers Next: Error Codes β†’