Overviewβ
When a card transaction occurs, the OPCard platform asynchronously delivers transaction event notifications to the merchant's configured callback URL via Webhook. The merchant's server is responsible for receiving and processing these notifications to keep transaction statuses synchronized, perform reconciliation, and enable risk monitoring.
Typical Use Cases:
- Keep your internal system synchronized with card transaction statuses in real time.
- Receive notifications for key events, including refunds, chargebacks, and card loads.
- Maintain a unified record of fund movements for reconciliation and financial tracking.
Notification Mechanismβ
Notification Flowβ
ββββββββββββββββ Transaction Completed ββββββββββββββββ HTTP POST βββββββββββββββββββ
β OPCard β ββββββββββββββββββββββ β Webhook β βββββββββββ β Merchant β
β β β Notification β + Signature β Webhook Endpointβ
ββββββββββββββββ β Service β Headers βββββββββββββββββββ
ββββββββββββββββ β
β Signature Verification
β
ββββββββββββββββββββββ
β Return HTTP 200 OK β
ββββββββββββββββββββββ
Key Specificationsβ
| Item | Description |
|---|---|
| Delivery Method | HTTP POST (application/json) |
| Webhook Endpoint | Configure Webhook URL |
| Retry Policy | Up to 12 retry attempts. The retry intervals are: 1m, 2m, 4m, 8m, 15m, 30m, 1h, 2h, 4h, 8h, 16h, and 32h. |
| Delivery Trigger | Notifications are sent in real time whenever the transaction status changes. |
| Success Response | The merchant's server must return HTTP 200. Any other response is considered a delivery failure. |
| Idempotency | Each notify_event_id is globally unique. Merchants should store and deduplicate this value to ensure idempotent processing. |
| Character Encoding | UTF-8 |
| Time Zone | UTC (convert to your local time zone if required). |
Event Flow Descriptionβ
This section describes the complete lifecycle of subscription events, from event generation to webhook delivery and merchant processing.
Notification Parametersβ
| Parameter | Type | Length | Required | Description | Example |
|---|---|---|---|---|---|
event_code | string | - | Required | Identifies the specific type of event.
| 165801 |
event_type | string | - | Required | Event type
| card.transaction.success |
notify_event_id | long | - | Required | A unique notification event ID used for idempotent processing, log tracing, and troubleshooting. | 2062493598283964416 |
+event_payload | object | - | Required | An object containing the business data associated with the event, including detailed card transaction information. |
Signature Verificationβ
Signature Algorithm and Request Headersβ
| Item | Value |
|---|---|
| Signature Algorithm | HMAC-SHA256 (default) / SHA256withRSA |
| Signature Generation Rule | HMAC(secret_key, timestamp + "\n" + payload) |
| HTTP Request Headers | X-ODP-Timestamp, X-ODP-Signature |
Signature Verification Steps (Required on Merchant Server)β
# Python
import hmac
import hashlib
def verify_webhook(headers: dict, body: bytes, secret_key: str) -> bool:
timestamp = headers.get("X-ODP-Timestamp", "")
signature = headers.get("X-ODP-Signature", "")
# 1. Concatenate the signature string (the separator must be `"\n"`, not an empty string).
message = timestamp + "\n" + body.decode("utf-8")
# 2. HMAC-SHA256
expected = hmac.new(
secret_key.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256
).hexdigest()
# 3. Verify the signature
return hmac.compare_digest(expected, signature)
Signature Verification Notesβ
| Risk | Mitigation |
|---|---|
| Incorrect separator used in signature payload | The separator must be "\n" (newline character). Do not use an empty string or space. |
| Secret key leakage | The secret_key must never be hardcoded in the frontend or client-side applications. |
| Replay attacks | Validate the time difference between X-ODP-Timestamp and the server time (recommended β€ 5 minutes). |
| Request body modification | Signature verification must be performed against the original request body. Do not parse or modify the body before verification. |
Webhook Payload Exampleβ
Headerβ
X-ODP-Timestamp: 1752019200
X-ODP-Signature: 4f7e3d2c1b0a9f8e7d6c5b4a3f2e1d0c...
Bodyβ
- Transaction success
- Transaction failure
- Authorization success
- Authorization failure
- Card load event
{
"event_code": "165801",
"event_type": "card.transaction.success",
"notify_event_id": 2076942421039628288,
"event_payload": {
"billing_amount": "HKD 0.09",
"creation_time": "2026-04-03 15:04:25",
"transaction_id": "1112002320282",
"account_id": "1016X10007748930412",
"program_value": 1,
"transaction_amount": "HKD 0.09",
"transaction_source": "Online",
"merchant_name": "401 Congress",
"transaction_type": "Purchase",
"card_number": "545502 **** 1159",
"originalTransactionId": "1112002320265",
"card_id": "1"
}
}
{
"event_code": "165803",
"event_type": "card.transaction.failure",
"notify_event_id": 2077647521291722752,
"event_payload": {
"billing_amount": "HKD 0.09",
"creation_time": "2026-04-03 15:04:25",
"transaction_id": "1112002320292",
"account_id": "1016X10007748930412",
"program_value": 8,
"transaction_amount": "HKD 0.09",
"transaction_source": "In Wallet ATM",
"merchant_name": "401 Congress",
"error_cause": "test",
"transaction_type": "Load",
"card_number": "545502 **** 1159",
"card_id": "1"
}
}
{
"event_code": "165804",
"event_type": "card.authorization.success",
"notify_event_id": 2077672457305612288,
"event_payload": {
"billing_amount": "HKD 0.09",
"creation_time": "2026-04-03 15:04:25",
"account_id": "1016X10007748930412",
"program_value": 8,
"transaction_amount": "HKD 0.09",
"merchant_name": "401 Congress",
"error_cause": "test",
"card_number": "545502 **** 1159",
"card_id": "1"
}
}
{
"event_code": "165805",
"event_type": "card.authorization.failure",
"notify_event_id": 2077672457305612288,
"event_payload": {
"billing_amount": "HKD 0.09",
"creation_time": "2026-04-03 15:04:25",
"account_id": "1016X10007748930412",
"program_value": 8,
"transaction_amount": "HKD 0.09",
"merchant_name": "401 Congress",
"error_cause": "test",
"card_number": "545502 **** 1159",
"card_id": "1"
}
}
{
"event_code": "165802",
"event_type": "card.load",
"notify_event_id": 2076942428406169600,
"event_payload": {
"billing_amount": "HKD 0.09",
"creation_time": "2026-04-03 15:04:25",
"transaction_id": "1112002320297",
"account_id": "1016X10007748930412",
"program_value": 8,
"transaction_type": "Exchange",
"card_id": "1"
}
}
Frequently Asked Questions (FAQ)β
Q1: Is HTTPS required for the webhook URL?
A: Yes. HTTPS is required for the production environment, and the SSL/TLS certificate must be valid.
Q2: What happens if the merchant does not return HTTP 200?
A: The notification will be added to the retry queue. Merchants are advised to return a response within 4 seconds and process business logic asynchronously.
Q3: Can webhook notifications arrive out of order?
A: Yes, it is possible. Merchants should use transaction_id for transaction correlation and should not rely on the notification order.
Q4: Can the same transaction be notified multiple times?
A: Yes. Merchants should use notify_event_id for idempotency and process each event ID only once.
Q5: Is there a limit on notification frequency?
A: There is currently no rate limit. However, merchants should ensure that the response is returned within 3 seconds.
Q6: Is transaction_amount always formatted with two decimal places?
A: Yes. Merchants should use BigDecimal for parsing to avoid floating-point precision issues.
Q7: Is the full card number transmitted?
A: No. card_no is not the actual card number. It is an internal card identifier or card reference used to identify the card.