Payment Webhooks
When a player attempts to pay for an order, a payment is created. Aghanim's payment events webhook notifies your game of any status changes for payments linked to the order, providing detailed information across all possible states for analytics and tracking.
This webhook is activated through the Payment events event, selectable in Game → Webhooks.
A single order can be associated with multiple payments if the player made multiple attempts to pay that either failed or were not completed.
Payment events may be sent multiple times for the same payment ID when payment details (amounts, commissions, taxes) are updated after the initial event. This can happen even if the payment status remains the same, as these updates affect revenue tracking and analytics.
When handling payment events:
- Use the
idempotency_keyto identify duplicate events - Either ignore duplicate events or update your records with the latest payment information
- Do not raise errors for duplicate events, as this will trigger webhook retries based on the retry schedule
- Note that multiple payment attempts for the same order will have different payment IDs (
idfield), not duplicate events - A won dispute re-emits
payment.succeededwith a newidempotency_key. It is a separate event reporting the dispute outcome, not a retry of the original success: check thedisputeobject to tell them apart
Payment Statuses
The payment webhook can be triggered for any of the following payment statuses:
succeeded– The payment was successfully completed and the funds were deducted from the user's account. This event is also emitted when a previously-disputed payment is resolved in the merchant's favor: in that case the payload carries a non-nulldisputeobject withresult: won.abandoned– The payment was canceled by the user.chargeback– The payment was canceled after the chargeback dispute was resolved in the player's favor. The payload carries adisputeobject withresult: lost.declined– The payment could not be completed because the user's payment method was declined.dispute– The user filed a dispute with their payment method regarding this payment.expired– The payment provider reported the payment as expired because it wasn't completed in time.pending– The payment was initiated by the user and is pending completion.refunded– The payment was refunded and the funds returned to the user.rejected– The payment was flagged as potentially fraudulent and was not processed.voided– The payment was voided before any funds were transferred.
Requirements
To use the payment events webhook from Aghanim, you should have the webhook server configured as follows:
- HTTPS endpoint, accepting POST webhook requests.
- Listen for events, generated and signed by Aghanim.
- Handle the
idempotency_keyincluded in the webhook payload to prevent processing duplicate webhooks. - Respond with 2xx status codes if the payment event was processed successfully, and 4xx or 5xx for denial or errors.
Configuration
- Develop a function for the
payment.*webhook processing. - Make your endpoint available.
- Register your endpoint within Aghanim account → Game → Webhooks → Add webhook by choosing the payment events event type.
Alternatively, you can register your endpoint within Aghanim using the Create Webhook API method.
Request schema
Below is an example of an payment.succeeded webhook request:
- HTTP
- cURL
POST /your/webhook/uri HTTP/1.1
Content-Type: application/json
Host: your-webhook-endpoint.com
User-Agent: Aghanim/0.1.0
X-Aghanim-Signature: 2e45ed4dede5e09506717490655d2f78e96d4261040ef48cc623a780bda38812
X-Aghanim-Signature-Timestamp: 1725548450
{
"event_type": "payment.succeeded",
"event_data": {
"id": "pmt_eFgYpxryeKXpLKfmZstI",
"order_id": "ord_eCacpFwavzi",
"items": [
{
"id": "itm_exTBZQmIlDz",
"name": "Crystals",
"description": "Reign supreme over your rivals with this massive crystal treasure.",
"sku": "crystals",
"quantity": 480000,
"price": 9499,
"price_decimal": 94.99,
"currency": "USD",
"type": "item",
"nested_items": null,
"source": "order"
}
],
"receipt_number": "2409051289614565",
"status": "succeeded",
"amount": 94,
"amount_decimal": 94.99,
"currency": "USD",
"payment_method": "cards",
"created_at": 1725547595,
"modified_at": 1725547657,
"metadata": null,
"decline_reason_code": null,
"decline_reason": null,
"three_d_secure_result": "authenticated",
"three_d_secure_flow": "challenge"
},
"event_time": 1725548450,
"event_id": "whevt_eCacGbJVbvToOgzjXUgOCitkQE",
"idempotency_key": "idmpt_aXRlb...JkX2VFS",
"request_id": "d1593e9c-c291-4004-8846-6679c2e5810b",
"sandbox": false,
"trigger": "checkout.purchase",
"transaction_id": "whtx_eCacGbJVbvT",
"context": null,
"game_id": "gm_exTAyxPsVwh"
}
curl "https://your-webhook-endpoint.com/your/webhook/uri" \
-X POST \
-H "Content-Type: application/json" \
-H "User-Agent: Aghanim/0.1.0" \
-H "X-Aghanim-Signature: 2e45ed4dede5e09506717490655d2f78e96d4261040ef48cc623a780bda38812" \
-H "X-Aghanim-Signature-Timestamp: 1725548450" \
-d '{
"event_type": "payment.succeeded",
"event_data": {
"id": "pmt_eFgYpxryeKXpLKfmZstI",
"order_id": "ord_eCacpFwavzi",
"items": [
{
"id": "itm_exTBZQmIlDz",
"name": "Crystals",
"description": "Reign supreme over your rivals with this massive crystal treasure.",
"sku": "crystals",
"quantity": 480000,
"price": 9499,
"price_decimal": 94.99,
"currency": "USD",
"type": "item",
"nested_items": null,
"source": "order"
}
],
"receipt_number": "2409051289614565",
"status": "succeeded",
"amount": 94,
"amount_decimal": 94.99,
"currency": "USD",
"payment_method": "cards",
"created_at": 1725547595,
"modified_at": 1725547657,
"metadata": null,
"decline_reason_code": null,
"decline_reason": null,
"three_d_secure_result": "authenticated",
"three_d_secure_flow": "challenge"
},
"event_time": 1725548450,
"event_id": "whevt_eCacGbJVbvToOgzjXUgOCitkQE",
"idempotency_key": "idmpt_aXRlb...JkX2VFS",
"request_id": "d1593e9c-c291-4004-8846-6679c2e5810b",
"sandbox": false,
"trigger": "checkout.purchase",
"transaction_id": "whtx_eCacGbJVbvT",
"context": null,
"game_id": "gm_exTAyxPsVwh"
}'
The Event schema
| Key | Type | Description |
|---|---|---|
event_id | string | Unique Event ID generated by Aghanim. |
game_id | string | Your game ID in the Aghanim system. |
event_type | string | The type of the event, payment.succeeded in this case. |
event_time | number | The time the event was sent, in Unix epoch time. May differ across duplicate deliveries of the same event. |
event_data | EventData | Contains the event-specific data, with possible keys for inherited objects. |
idempotency_key | string | Ensures webhook actions are executed only once, even if retried. |
request_id | string|null | If the event was triggered by an API request, the request ID is included. |
sandbox | boolean | Indicates whether the event was sent from the sandbox game environment. |
trigger | string|null | The trigger that caused the event to be sent. |
transaction_id | string | The transaction ID generated by Aghanim. This ID may be the same for multiple events emitted within the same transaction. |
context | object|null | Contextual information about the event. |
The EventData schema
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the payment |
order_id | string | Unique identifier for the associated order |
items | Item[] | Includes details of the items in the associated order. See The Item schema |
receipt_number | string | Human-readable receipt number |
status | enum | Current payment status (succeeded, abandoned, chargeback, declined, dispute, expired, pending, refunded, rejected, voided) |
amount | integer | [Deprecated] Payment amount in major currency units, truncated to a whole number. Use amount_decimal for the exact amount. |
amount_decimal | number|null | Exact payment amount in major currency units |
currency | string | Payment currency |
payment_method | string | Payment method used (cards, apple_pay, google_pay, paypal, etc.) |
created_at | integer | Unix timestamp when the payment was created |
modified_at | integer | Unix timestamp when the payment was last modified |
metadata | object|null | The metadata object structured as key-value pairs to store additional information about the payment. |
decline_reason | string|null | Human-readable description of the payment decline. See Decline reason codes |
decline_reason_code | string|null | Numeric decline code identifying the decline reason. See Decline reason codes |
three_d_secure_result | string|null | 3DS authentication result (not_attempted, authenticated, failed) |
three_d_secure_flow | string|null | 3DS authentication flow used during the payment attempt (none, challenge, frictionless) |
dispute | object|null | The resolved dispute this event reports: set on payment.succeeded (dispute won) and payment.chargeback (dispute lost) for a disputed payment, null otherwise. See The Dispute object |
The Item schema
| Key | Type | Description |
|---|---|---|
id | string | Item ID generated by Aghanim. |
name | string | Item name. |
description | string|null | Item description. |
sku | string | Item SKU matching on both the game and Aghanim sides. |
quantity | number | Item quantity. |
price | number|null | Item price in minor currency units. If the item is free, this field is null. |
price_decimal | number|null | Item price in decimal units. If the item is free, this field is null. |
currency | string|null | Item price currency. If the item is free, this field is null. |
type | string | The type of item. Possible values: item, bundle. |
nested_items | NestedItem[]|null | An array of items included in the bundle. |
fallback_item | Item|null | The item that may be given as fallback if the main item can't be given to the player. |
source | string | Identifies why the item is being delivered. Possible values: order, bonus, bundle, coupon, lootbox, free_item, liveops, daily_reward, loyalty_reward, progression_program, rolling_offer, pick_one_offer. |
metadata | object|null | Custom key-value pairs for the item to support additional logic on your end. |
Item type bundle can contain several nested items inside.
You can handle the common sku of the bundle or handle each nested item individually.
The NestedItem schema
| Key | Type | Description |
|---|---|---|
id | string | Item ID generated by Aghanim. |
name | string | Item name. |
description | string|null | Item description. |
sku | string | Item SKU matching on both the game and Aghanim sides. |
quantity | number | Item quantity. |
fallback_item | Item|null | The item that may be given as fallback if the main item can't be given to the player. |
metadata | object|null | Custom key-value pairs for the item to support additional logic on your end. |
Decline reason codes
When a payment ends in declined, the payload carries a decline_reason_code and its human-readable decline_reason.
decline_reason_code | decline_reason | Description |
|---|---|---|
10 | Insufficient funds | The player does not have sufficient funds in their account to complete the payment. |
20 | Payment expired | The payment was not completed within the allowed time and expired. |
30 | Suspected fraud | The payment was declined by the payment provider due to suspected fraudulent activity. |
40 | 3D Secure authentication failed | The player failed or was unable to complete 3D Secure authentication. |
50 | Payment method country mismatch | The country of the player's payment method does not match the country allowed for this payment. The user was prompted to change the country at checkout and try again. |
60 | Payment limit exceeded | The payment exceeds the limit set for the player's account or payment method. |
70 | Blocked by cardholder | The payment type or payment method was blocked by the cardholder. |
80 | Restricted account | The player's account is subject to restrictions imposed by the bank or payment system, preventing the payment from being completed. |
90 | Incorrect card security code | The player entered an incorrect card security code (CVV/CVC). |
100 | Invalid account number | The account or card number entered by the player is invalid. |
110 | Previous payment attempt not completed | The player's previous payment attempt has not yet been completed. |
120 | Expired card | The player's card has expired. |
130 | Closed account | The account linked to the player's payment method has been closed. |
140 | Invalid payment code | The code provided by the payment method to complete the payment is invalid. |
150 | Lost card | The player's card was reported as lost by the bank. |
160 | Stolen card | The player's card was reported as stolen by the bank. |
170 | Payment not permitted | The player's bank or payment provider did not authorize this payment. |
180 | Canceled by user | The player canceled the payment before it was completed. |
200 | Refunds not supported for this payment method | The payment method used by the player does not support refunds. |
210 | Payment declined | The player's payment was declined by the bank or payment provider without a more specific reason being provided. |
210 is the generic fallback used when the provider gives no more specific reason. Handle unknown codes gracefully, as this list may grow over time.
The Dispute object
| Field | Type | Description |
|---|---|---|
result | enum | Outcome of the dispute: won (the payment stays succeeded) or lost (the funds were returned to the player) |
result_at | integer|null | Unix timestamp when the dispute was resolved, null when unavailable |
Need help?
Contact our integration team at integration@aghanim.com