Webhooks are the authoritative event stream for card activity, ledger movements, and user wallet changes. This guide walks through a robust ingestion pipeline.

Delivery Contract

Header Value Notes
Content-Type application/json Always UTF-8 JSON.
x-client-id Your tenant ID Mirrors the API credential.
x-signature HMAC-SHA256 sha256=<hex> computed with the client secret.
x-request-id UUID Mirrors incoming X-Request-ID. Use for support.
x-idempotency-key Optional Propagates from source API call when available.

Payload schema:

{
  "event": "card_transaction",
  "data": {
    "id": "5b2fa934-1f1d-4b71-8d5a-a3e2f61ac1af",
    "cardId": "0b1e9c6e-5d87-4f90-8c4d-0ad6f4ce4be5",
    "transactionAmount": "12.34",
    "transactionCurrency": "USD",
    "type": "authorization",
    "referenceId": "c8de3ebf-5b2d-4020-a7bb-65f88c3a37ce",
    "timestamp": "2025-06-02T11:24:12Z"
  }
}

Recommended Ingestion Flow

  1. Verify signature immediately.
  2. Persist raw payload (S3, Postgres, BigQuery) keyed by data.id.
  3. Detect duplicates (data.id or x-idempotency-key).
  4. Acknowledge quickly (204 No Content).
  5. Dispatch asynchronously to business logic consumers.
func HandleWebhook(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    defer r.Body.Close()

    if !verifySignature(body, r.Header.Get("x-signature"), secret) {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }

    var evt Envelope
    if err := json.Unmarshal(body, &evt); err != nil {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }

    if err := store.Insert(r.Context(), evt.Data.ID, body); err != nil {
        if errors.Is(err, ErrDuplicate) {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        http.Error(w, "storage error", http.StatusInternalServerError)
        return
    }

    queue.Publish(evt) // async processing
    w.WriteHeader(http.StatusNoContent)
}

How PayCA Delivers Webhooks

  • We capture every ledger or card event inside a Temporal workflow named client-hook-<type>-<txID> – this keeps retries and ordering consistent per transaction.
  • The workflow looks up your active webhook URLs for that event type and records a pending row in client_hook_statuses for each destination before the first attempt.
  • Payloads are JSON-encoded and signed with HMAC-SHA256 using your current client secret; the signature is sent back in the x-signature header.
  • Delivery is a plain POST request with optional propagation of the originating API call's idempotency key via x-idempotency-key when PayCA has it.
  • Temporal retries on any non-2xx response or transport failure with exponential backoff (5s → 10s → 20s … capped at 30m) for up to 5 attempts; a success updates the status row to sent, while a final failure leaves it as failed.

Event Types

Event Fired When Key Fields
card_transaction Authorizations, settlements, adjustments, refunds, card lifecycle events cardId, type, referenceId, linkedId
account_transaction Mirror/revenue/card ledger changes accountId, type, subtype, amount
user_account_transaction User wallet deposits/withdrawals userAccountId, type, referenceId

Link card_transaction.referenceId to account_transaction.referenceId to see both sides of a movement.

Signature Verification

func verifySignature(body []byte, signature, secret string) bool {
    parts := strings.SplitN(signature, "=", 2)
    if len(parts) != 2 || parts[0] != "sha256" {
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return subtle.ConstantTimeCompare([]byte(expected), []byte(parts[1])) == 1
}

Reject invalid signatures with 401. PayCA retries (up to 5 attempts) when it receives non-2xx responses.

Replay Strategy

If your consumer falls behind:

curl -X POST "$PAYCA_BASE_URL/v1/webhooks/resend?fromDate=2025-06-01T00:00:00Z" \
  -H "x-client-id: $PAYCA_CLIENT_ID" \
  -H "x-client-secret: $PAYCA_CLIENT_SECRET"

Recommendations:

  • Replay in chronological order.
  • Rate-limit the resend queue so you do not trigger 429.
  • Include monitoring so a long outage triggers alerts.
  • The resend endpoint scans the client_hook_statuses table for rows still marked failed and requeues a lightweight workflow (client-hook-resend-<type>-<clientID>-<uuid>) per unique transaction so you do not receive duplicates.
  • Temporal handles the same 5-attempt retry loop during resend. As soon as your endpoint returns 2xx the status is flipped to sent and future resends skip that transaction.
  • Use the account, user, and card counters in the JSON response to confirm how many jobs were enqueued; the total field is the sum of those buckets.

When to Trigger a Resend

  • Your webhook receiver was unavailable long enough for PayCA's automatic retries to exhaust (status remains failed).
  • You deployed a bug fix that previously caused 4xx responses and want to backfill only the affected time window using the optional fromDate filter.
  • You purposefully paused ingestion (e.g. maintenance) and need to flush the backlog after re-enabling your pipeline.

Storage Schema

Minimal schema for PostgreSQL:

CREATE TABLE payca_webhooks (
  id UUID PRIMARY KEY,
  event TEXT NOT NULL,
  payload JSONB NOT NULL,
  delivered_at TIMESTAMPTZ NOT NULL,
  request_id UUID,
  card_id UUID,
  account_id UUID,
  user_account_id UUID
);

Denormalize card_id/account_id/user_account_id to accelerate dashboards. Index by delivered_at for retention policies.

Alerting & Dashboards

  • Alert if retries exceed 3 attempts (account_transaction with status=failed).
  • Track webhook latency (delivery timestamp vs ingestion timestamp).
  • Visualize spend, declines, and refunds from webhook payloads to support operations.

Security Best Practices

  • Serve webhook endpoints over HTTPS with TLS 1.2+.
  • Restrict inbound IPs to PayCA’s documented ranges.
  • Rotate secrets quarterly; update the receiver without downtime by accepting both old/new secrets during the overlap window.

Continue with the Sandbox Playbook to generate webhook payloads on demand, or see Fees & Revenue Recognition for financial reporting.