Skip to content

Latest commit

 

History

History
118 lines (95 loc) · 4.05 KB

File metadata and controls

118 lines (95 loc) · 4.05 KB

Callback Contract

Paystable delivers final hold outcomes to the merchant callback_url with a signed HTTP POST. The merchant backend should use this callback as the trusted trigger for fulfillment, release, or manual escalation.

Frontend polling and SSE are useful for display. They should not be the source of fulfillment truth.

Request

POST <callback_url>
Content-Type: application/json
X-Paystable-Signature: sha256=<hex-hmac>
X-Paystable-Idempotency-Key: <opaque-key>
X-Paystable-Timestamp: <unix-seconds>

Payload

{
  "txn_id": "order_abc123",
  "event": "transaction.confirmed",
  "status": "CONFIRMED",
  "amount": 49900,
  "currency": "INR",
  "gateway": "payu",
  "verified_at": "2026-06-24T12:00:19Z",
  "metadata": {
    "order_id": "order_abc123"
  }
}

For review states, Paystable includes additional context:

{
  "txn_id": "order_abc123",
  "event": "transaction.mismatch",
  "status": "MISMATCH",
  "amount": 49900,
  "currency": "INR",
  "gateway": "payu",
  "verified_at": "2026-06-24T12:00:19Z",
  "reason": "amount_mismatch",
  "gateway_amount": 39900,
  "hold_amount": 49900,
  "metadata": {}
}

When an operator resolves a review as confirmed or failed, Paystable sends the same final event shape with "reason":"manual_review" and "resolution_source":"manual_review". A no_action decision is recorded in the audit ledger and does not send a callback. Repeating the same review request is accepted without queuing another delivery.

Field Type Notes
txn_id string Merchant-supplied hold ID.
event string transaction.confirmed, transaction.failed, transaction.indeterminate, or transaction.mismatch.
status string CONFIRMED, FAILED, INDETERMINATE, or MISMATCH.
amount int64 Hold amount in smallest currency unit.
currency string ISO 4217. Current normal value: INR.
gateway string Gateway name. Current adapter: payu.
verified_at string RFC 3339 timestamp generated by Paystable.
metadata object Original hold metadata.
reason string Present for failure/review cases when Paystable has a reason.
gateway_amount int64 Present for amount review cases.
hold_amount int64 Present for amount review cases.
resolution_source string manual_review when an operator produced the final outcome.

Signature Verification

Paystable signs the raw request body with HMAC-SHA256 using MERCHANT_CALLBACK_SECRET. The header value is hex encoded and prefixed with sha256=.

Verify the raw bytes before JSON parsing:

func verify(body []byte, header, secret string) bool {
	if !strings.HasPrefix(header, "sha256=") {
		return false
	}
	got, err := hex.DecodeString(strings.TrimPrefix(header, "sha256="))
	if err != nil {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write(body)
	return hmac.Equal(mac.Sum(nil), got)
}

Idempotency

Paystable delivers with at-least-once semantics. The same event may be sent more than once if the merchant endpoint times out, returns a retryable status, or the network fails after processing.

Rules for merchants:

  • Treat X-Paystable-Idempotency-Key as an opaque string.
  • Store it before taking irreversible action.
  • If the key was already processed, return 2xx and do nothing.
  • Never infer business meaning from the key format.

Response Contract

Merchant response Paystable behavior
2xx Marks delivery complete.
4xx except 429 Treats as permanent, marks exhausted, alerts ops.
429, 5xx, timeout, connection refused Retries with backoff until max_attempts.

Respond within DELIVERY_TIMEOUT_S seconds. Default: 10.

Security Checklist

  • Verify X-Paystable-Signature on the raw body.
  • Reject old timestamps if your app has replay protection.
  • Deduplicate by X-Paystable-Idempotency-Key.
  • Return 2xx only after your database transaction commits.
  • Use HTTPS callback URLs in production.
  • Set DELIVERY_ALLOW_INSECURE_CALLBACK=true only for local development.