JavaScript
Integrate the Aghanim to start accepting payments for your web game items online through a prebuilt checkout page. The Checkout with JavaScript uses our JavaScript SDK, which runs in the browser and needs no server-side runtime.
Register with Aghanim and link your game
First, register for an Aghanim account. At the end of registration, add the link to your game. For a web game, that is the URL players open in their browser.
Set up environment
If you want to make real payments, you are all set as the live mode is used as default. Otherwise, use a sandbox, an isolated test environment, to simulate the Aghanim events to test payments without real money movement. To turn on the sandbox mode, set the Sandbox toggle to the active position.
Sandbox supports card payments only. Alternative methods (PayPal, wallets, local payment methods) appear only in live. See Test payments for the test cards.
While integrating, you will need an SDK key to authenticate requests to the Aghanim. Keep in mind that the sandbox and live modes have different keys. Find the SDK key in Integration → API keys.
Configure game client-side
Configure your game client to work with the Checkout by setting up the SDK and implementing the necessary code to process its methods.
Install SDK
Pull the SDK in with a package manager, or drop it in with a script tag if your game has no build step. Both give you the same Aghanim object.
- npm
- Script tag
npm install @aghanim-sdk/checkout
Import it where you initialize the SDK.
import { Aghanim } from "@aghanim-sdk/checkout";
Load the SDK from the CDN, before the script that boots your game.
<script src="https://cdn.jsdelivr.net/npm/@aghanim-sdk/checkout/dist/aghanim-checkout.global.js"></script>
The bundle exposes Aghanim as a global, so there is nothing to import. Pin a version for production (@aghanim-sdk/checkout@<version>/dist/...) so a new release can't reach your players before you have tested it.
Initialize SDK
Create the SDK instance once, when your game boots.
const aghanim = Aghanim.init({ apiKey: "sdk_..." });
Configure player ID
The SDK holds the player ID on the instance and uses it in all following method calls. When your game client knows who the player is, set it once.
aghanim.setPlayerId("player_1");
Web games do not restart between accounts the way a mobile app does, so clear the ID when the player signs out.
aghanim.clearPlayerId();
Create item
The integration needs the items to be added to the Dashboard. When creating items, each should have its SKU, a unique identifier for the item within your game backend. You can add their prices, currency, sale configuration, and more.
To add an item to the Dashboard:
- Go to SKU Management → Items.
- Click Add Item. The site will open the Add Item page.
- Enter the item name New item.
- Enter the item SKU items.new.ba68a028-2d51-46b4-a854-68fc16af328a.
- In the Price block:
- Select the Fiat price type for a real money item.
- Enter the price 1.99.
- Click Add item.
For integration purposes, we have shortened an item setup. Before going live, use every suitable feature while adding items to the Dashboard.
Create Order
An Order is the programmatic representation of what the player sees in the payment form. Every Order is associated with a player and one or more items, each identified by its SKU.
You can create Orders from the game client with this SDK, or on your backend. Both produce the same Checkout.
- Client-side
- Server-side (S2S)
Create the Order straight from the game client. This is the shorter path, and it fits straightforward SKU purchases. The backend prices everything server-side regardless, so the client never dictates amounts.
const order = await aghanim.orders.create({
items: [{ sku: "gems-100" }],
locale: "en",
metadata: { level: "12" },
});
The Order is created for the player you set in Configure player ID.
Have your backend call the Create Order endpoint with the S2S key, then hand the returned checkout_url to the client. Prefer this when prices or entitlements are decided by your backend, or when you need to attach metadata the client should not control.
checkout_url as opaqueUse the checkout_url value as-is. Do not parse it, modify it, append parameters, or rebuild it from order_id. The presence of the field is part of the public contract; the internal structure of the URL is not, and may change.
Launch Checkout
Open the Checkout and subscribe to its events. Pass either the Order you created on the client or the opaque checkout_url your backend created over S2S.
- Client-side Order
- Server-side checkout_url
const checkout = await aghanim.openCheckout(order);
checkout
.onPaid(() => showSuccessUI())
.onClosed(() => game.resume());
Have your backend return the checkout_url from its Create Order call, then hand that string to the SDK unchanged. The SDK reads the Order ID out of the URL, checks the Order is still payable, and opens it the same way as a client-created Order.
// Your own game backend. It calls Create Order with the S2S key and returns
// the checkout_url from the response.
const { checkout_url } = await fetch("/api/shop/buy", {
method: "POST",
body: JSON.stringify({ sku: "gems-100" }),
}).then((res) => res.json());
const checkout = await aghanim.openCheckout(checkout_url);
checkout
.onPaid(() => showSuccessUI())
.onClosed(() => game.resume());
A few things differ on this path:
- Pass the URL as it came back, unchanged.
- An Order that is expired, canceled, or already paid rejects
openCheckout()withorder_not_payablebefore anything opens, so show your own message instead of framing an error screen. - Payment events arrive the same way as on the client-created path.
- Grant from the
item.addwebhook. Your backend created the Order, so it is the side that learns about the payment first.
The Checkout opens as a modal iframe over your game by default. See Presentation modes to embed it in your own store panel or open it in a separate window.
Apple verifies Apple Pay per top-level page domain, not per iframe. When the Checkout runs framed inside your page, the domain in the player's address bar is your game domain, so that domain has to serve Apple's association file.
Aghanim generates the file for you. Request it from your Aghanim contact, then host it unmodified at:
https://<your-game-domain>/.well-known/apple-developer-merchantid-domain-association.txt
- Serve it over HTTPS from every domain and subdomain that hosts the game page, with HTTP
200,Content-Type: text/plain, no redirect and no auth wall. - Keep it byte-identical to the file we hand you, and leave it in place. Apple re-validates periodically, and we re-issue the file when it rotates.
Until this resolves, Apple Pay is absent from the payment-method list on your domain and the other methods are unaffected. Opening the Checkout in a separate window needs no file, because the Checkout is then a top-level page on an already-verified Aghanim domain.
If the Checkout falls back to a full-page redirect, the player comes back through the Order's back_to_game_url, which supports an {order_id} placeholder. Set it when you create the Order.
Check unconsumed Orders
A player can close the tab mid-payment and come back later, so the paid event alone is not a guarantee your game saw every purchase. Orders created by this SDK are consumable: once paid, they stay in an unconsumed list until your game acknowledges them. Read that list on boot, on resume, and after paid.
const orders = await aghanim.orders.unconsumedDetails();
Consume paid Orders
When you know which Orders the player has paid for, grant the items in your game logic, then mark each Order consumed to remove it from the list. Consuming the same Order twice fails, so always grant before you consume.
for (const order of orders) {
grantToPlayer(order.items);
await aghanim.orders.consume(order.id);
}
If your game has a backend, grant from the signed webhook instead and use this list only to reconcile. See Handle post-payment events on game server-side.
Full implementation code
import { Aghanim } from "@aghanim-sdk/checkout";
const aghanim = Aghanim.init({ apiKey: "sdk_..." });
// As soon as your game knows who the player is.
aghanim.setPlayerId("player_1");
// Grant anything the player paid for but has not received yet.
async function grantPendingOrders() {
const orders = await aghanim.orders.unconsumedDetails();
for (const order of orders) {
grantToPlayer(order.items);
await aghanim.orders.consume(order.id);
}
}
// On boot and whenever the game regains focus.
await grantPendingOrders();
// When the player taps "Buy".
async function buy(sku) {
const order = await aghanim.orders.create({
items: [{ sku }],
locale: "en",
});
const checkout = await aghanim.openCheckout(order);
checkout
.onPaid(() => showSuccessUI())
.onClosed(async () => {
await grantPendingOrders();
game.resume();
})
.onError((err) => {
if (err.code === "popup_blocked") showOpenPaymentPageButton();
});
}
Make payment
Make a payment. If you have set the sandbox mode, use the test card below. In the sandbox, you can make payments only with the test cards — alternative methods like PayPal, wallets, and local payment methods appear only in the live environment. The test cards accept any digits as CVV and any future date as expiry date. Don’t forget to fill in an email address to check the receipt is sent and any postal code as a billing address.
Successful payments
After you complete the payment, you will receive a receipt sent to the specified email address and a transaction record in Aghanim Dashboard → Transactions.
| Card Brand | Card Number | CVV | Expiry date | Country |
|---|---|---|---|---|
| VISA (credit) | 4242 4242 4242 4242 | Any 3 digits | Any future date | GB |
Unsuccessful payments
Make an unsuccessful payment just in case you are curious. You will see the transaction in Aghanim Dashboard → Transactions as well.
| Number | CVV | Expiry date | Response code | Description |
|---|---|---|---|---|
4832 2850 6160 9015 | Any 3 digits | Any future date | 16 | Payment declined |
For the live mode, you can find all supported payment methods in Company settings → Payment methods. Turn on or off those you see suitable. Some payment methods are available globally by default. You can’t disable Credit cards, Apple Pay, Google Pay, and PayPal.
In Checkout, the Aghanim evaluates the currency and any restrictions, then dynamically presents only the payment methods available to the player based on evaluation.
When you use the live mode, the payment form shows to the player a setting to save their payment method so they can make a one-click payment in the future.
Handle post-payment events on game server-side
To complete the Checkout, handle items’ granting and chargebacks on your game backend. To do so, implement a webhook system that accepts the item.add and item.remove webhooks. See the code example with the implementation.
Comply with the Aghanim requirements for these webhooks:
- Use HTTPS schema for the single POST webhook endpoint.
- Check that webhooks are generated and signed by the Aghanim.
- Handle the
idempotency_keyfield in the webhook payload to prevent processing duplicate webhooks. - Respond with the HTTP status codes:
2xxfor successfully processed webhooks.4xxand5xxfor errors.
Grant items to player
The Aghanim sends the item.add webhook to let you know about the purchased items and ask for your permission to grant them to the player.
When the Aghanim has your 2xx answer, it can complete the checkout logic and redirect the player to a deep link if provided.
Support refunds and chargebacks
The Aghanim sends the item.remove webhook when a bank or payment system reverses the transaction, or you have requested refund in Aghanim Dashboard → Transactions. Partial refunds are not supported.
Use suggested implementation
The suggested implementation handles the webhooks mentioned before:
item.addfor granting items. You need it for integration.item.removefor refunds and chargebacks. You might need it for integration.
- Python
- Ruby
- Node.js
- Go
# Use this sample code to handle webhook events in your integration.
#
# 1. Paste this code into a new file `server.py`.
#
# 2. Install dependencies:
# python -m pip install fastapi[all]
#
# 3. Run the server on http://localhost:8000
# python server.py
import fastapi, hashlib, hmac, json, typing
from fastapi.responses import JSONResponse
app = fastapi.FastAPI()
@app.post("/webhook")
async def webhook(request: fastapi.Request) -> dict[str, typing.Any]:
secret_key = "<YOUR_S2S_KEY>" # Replace with your actual webhook secret key
raw_payload = await request.body()
payload = raw_payload.decode()
timestamp = request.headers["x-aghanim-signature-timestamp"]
received_signature = request.headers["x-aghanim-signature"]
if not verify_signature(secret_key, payload, timestamp, received_signature):
raise fastapi.HTTPException(status_code=403, detail="Invalid signature")
data = json.loads(payload)
event_type = data["event_type"]
event_data = data["event_data"]
if event_type == "item.add":
add_item(event_data)
return {"status": "ok"}
if event_type == "item.remove":
remove_item(event_data)
return {"status": "ok"}
raise fastapi.HTTPException(status_code=400, detail="Unknown event type")
def verify_signature(secret_key: str, payload: str, timestamp: str, received_signature: str) -> bool:
signature_data = f"{timestamp}.{payload}"
computed_hash = hmac.new(secret_key.encode(), signature_data.encode(), hashlib.sha256)
computed_signature = computed_hash.hexdigest()
return hmac.compare_digest(computed_signature, received_signature)
def add_item(event_data: dict[str, typing.Any]) -> None:
# Placeholder logic for processing the event and adding item.
# In a real application, this function would interact with your database or inventory system.
player_id = event_data["player_id"]
for item in event_data["items"]:
sku = item["sku"]
print(f"Item {sku} has been credited to player's {player_id} account.")
def remove_item(event_data: dict[str, typing.Any]) -> None:
# Placeholder logic for processing the event and removing item.
# In a real application, this function would interact with your database or inventory system.
player_id = event_data["player_id"]
for item in event_data["items"]:
sku = item["sku"]
print(f"Item {sku} has been removed from player's {player_id} account.")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
# Use this sample code to handle webhook events in your integration.
#
# 1. Paste this code into a new file `server.rb`.
#
# 2. Install dependencies:
# gem install sinatra json hmac
#
# 3. Run the server on http://localhost:8000
# ruby server.rb
require 'sinatra'
require 'json'
require 'openssl'
post '/webhook' do
secret_key = "<YOUR_S2S_KEY>" # Replace with your actual webhook secret key
payload = request.body.read
timestamp = request.env["HTTP_X_AGHANIM_SIGNATURE_TIMESTAMP"]
received_signature = request.env["HTTP_X_AGHANIM_SIGNATURE"]
unless verify_signature(secret_key, payload, timestamp, received_signature)
halt 403, "Invalid signature"
end
data = JSON.parse(payload)
event_type = data["event_type"]
event_data = data["event_data"]
if event_type == "item.add"
add_item(event_data)
return { status: "ok" }.to_json
end
if event_type == "item.remove"
remove_item(event_data)
return { status: "ok" }.to_json
end
halt 400, "Unknown event type"
end
def verify_signature(secret_key, payload, timestamp, received_signature)
signature_data = "#{timestamp}.#{payload}"
computed_signature = OpenSSL::HMAC.hexdigest('sha256', secret_key, signature_data)
OpenSSL.secure_compare(computed_signature, received_signature)
end
def add_item(event_data)
# Placeholder logic for processing the event and adding item.
# In a real application, this function would interact with your database or inventory system.
player_id = event_data["player_id"]
for item in event_data["items"] do
sku = item["sku"]
puts "Item #{sku} has been credited to player's #{player_id} account."
end
end
def remove_item(event_data)
# Placeholder logic for processing the event and removing item.
# In a real application, this function would interact with your database or inventory system.
player_id = event_data["player_id"]
for item in event_data["items"] do
sku = item["sku"]
puts "Item #{sku} has been removed from player's #{player_id} account."
end
end
if __FILE__ == $0
require 'sinatra'
set :bind, '0.0.0.0'
set :port, 8000
end
// Use this sample code to handle webhook events in your integration.
//
// 1. Paste this code into a new file `server.js`.
//
// 2. Install dependencies:
// npm install express
//
// 3. Run the server on http://localhost:8000
// node server.js
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/webhook', express.raw({ type: "*/*" }), async (req, res) => {
const secretKey = '<YOUR_S2S_KEY>'; // Replace with your actual webhook secret key
const rawPayload = req.body;
const timestamp = req.headers['x-aghanim-signature-timestamp'];
const receivedSignature = req.headers['x-aghanim-signature'];
if (!verifySignature(secretKey, rawPayload, timestamp, receivedSignature)) {
return res.status(403).send('Invalid signature');
}
const payload = JSON.parse(req.body);
const { event_type, event_data } = payload;
if (event_type === 'item.add') {
addItem(event_data);
return res.json({ status: 'ok' });
}
if (event_type === 'item.remove') {
removeItem(event_data);
return res.json({ status: 'ok' });
}
return res.status(400).send('Unknown event type');
});
function verifySignature(secretKey, payload, timestamp, receivedSignature) {
const signatureData = `${timestamp}.${payload}`;
const computedSignature = crypto
.createHmac('sha256', secretKey)
.update(signatureData)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(receivedSignature));
}
function addItem(event_data) {
// Placeholder logic for processing the event and adding item.
// In a real application, this function would interact with your database or inventory system.
const playerId = event_data.player_id;
for (const item of event_data.items) {
const sku = item.sku;
console.log(`Item ${item.sku} has been credited to player's ${playerId} account.`);
}
}
function removeItem(event_data) {
// Placeholder logic for processing the event and removing item.
// In a real application, this function would interact with your database or inventory system.
const playerId = event_data.player_id;
for (const item of event_data.items) {
const sku = item.sku;
console.log(`Item ${item.sku} has been removed from player's ${playerId} account.`);
}
}
app.listen(8000, () => {
console.log('Server is running on http://localhost:8000');
});
// Use this sample code to handle webhook events in your integration.
//
// 1. Paste this code into a new file `server.go`.
//
// 2. Run the server on http://localhost:8000
// go run server.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
secretKey := "<YOUR_S2S_KEY>" // Replace with your actual webhook secret key
rawPayload, _ := ioutil.ReadAll(r.Body)
payload := string(rawPayload)
timestamp := r.Header.Get("X-Aghanim-Signature-Timestamp")
receivedSignature := r.Header.Get("X-Aghanim-Signature")
if !verifySignature(secretKey, payload, timestamp, receivedSignature) {
http.Error(w, "Invalid signature", http.StatusForbidden)
return
}
var data map[string]interface{}
if err := json.Unmarshal(rawPayload, &data); err != nil {
http.Error(w, "Invalid payload", http.StatusBadRequest)
return
}
eventType := data["event_type"].(string)
eventData := data["event_data"].(map[string]interface{})
if eventType == "item.add" {
addItem(eventData)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
return
}
if eventType == "item.remove" {
removeItem(eventData)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
return
}
http.Error(w, "Unknown event type", http.StatusBadRequest)
}
func verifySignature(secretKey, payload, timestamp, receivedSignature string) bool {
signatureData := fmt.Sprintf("%s.%s", timestamp, payload)
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(signatureData))
computedSignature := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(computedSignature), []byte(receivedSignature))
}
func addItem(eventData map[string]interface{}) {
// Placeholder logic for processing the event and adding item.
// In a real application, this function would interact with your database or inventory system.
playerID := eventData["player_id"].(string)
items := eventData["items"].([]interface{})
for _, item := range items {
itemMap := item.(map[string]interface{})
sku := itemMap["sku"].(string)
fmt.Printf("Item %s has been credited to player's %s account.\\n", sku, playerID)
}
}
func removeItem(eventData map[string]interface{}) {
// Placeholder logic for processing the event and removing item.
// In a real application, this function would interact with your database or inventory system.
playerID := eventData["player_id"].(string)
items := eventData["items"].([]interface{})
for _, item := range items {
itemMap := item.(map[string]interface{})
sku := itemMap["sku"].(string)
fmt.Printf("Item %s has been removed from player's %s account.\\n", sku, playerID)
}
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
fmt.Println("Server is running on http://localhost:8000")
http.ListenAndServe(":8000", nil)
}
Add webhook endpoint to Aghanim
When the webhook handling is ready, add the endpoint to the account so the Aghanim could start sending the events.
- Dashboard
- API
- Go to Integration → Webhooks.
- Click Add webhook. The site will open the Create webhook window.
- Copy and paste the URL
https://<YOUR_DOMAIN>/webhook. - Click Select events. The site will open the Select events to send window.
- Expand the Main class and select the Item add, Item remove checkboxes.
- Click Apply.
- Click Add. The site will redirect you to the webhook page.
- Click Back.
- cURL
- Python
- Ruby
- Node.js
- Go
curl -X POST https://api.aghanim.com/s2s/v1/webhooks \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <YOUR_S2S_KEY>' \
-d '{
"events": [
"item.add",
"item.remove"
],
"url": "https://<YOUR_DOMAIN>/webhook",
"description": "The endpoint for all webhooks",
"method": "POST",
"enabled": true,
"enabled_logs": true,
"player_context_enabled": true
}'
import requests
def create_webhook():
payload = {
"events": ["item.add", "item.remove"],
"url": "https://<YOUR_DOMAIN>/webhook",
"description": "The endpoint for all webhooks",
"method": "POST",
"enabled": True,
"enabled_logs": True,
"player_context_enabled": True
}
headers = {"Authorization": "Bearer <YOUR_S2S_KEY>", "Content-Type": "application/json"}
resp = requests.post("https://api.aghanim.com/s2s/v1/webhooks", json=payload, headers=headers)
return resp.json()
require 'net/http'
require 'json'
require 'uri'
def create_webhook
uri = URI("https://api.aghanim.com/s2s/v1/webhooks")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json', 'Authorization' => 'Bearer <YOUR_S2S_KEY>')
req.body = {
events: ["item.add", "item.remove"],
url: "https://<YOUR_DOMAIN>/webhook",
description: "The endpoint for all webhooks",
method: "POST",
enabled: true,
enabled_logs: true,
player_context_enabled: true
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
JSON.parse(res.body)
end
const axios = require('axios');
async function createWebhook() {
const payload = {
events: ["item.add", "item.remove"],
url: "https://<YOUR_DOMAIN>/webhook",
description: "The endpoint for all webhooks",
method: "POST",
enabled: true,
enabled_logs: true,
player_context_enabled: true
};
const res = await axios.post('https://api.aghanim.com/s2s/v1/webhooks', payload, {
headers: { 'Authorization': 'Bearer <YOUR_S2S_KEY>', 'Content-Type': 'application/json' }
});
return res.data;
}
package main
import (
"bytes"
"encoding/json"
"net/http"
)
func createWebhook() map[string]interface{} {
payload := map[string]interface{}{
"events": []string{"item.add", "item.remove"},
"url": "https://<YOUR_DOMAIN>/webhook",
"description": "The endpoint for all webhooks",
"method": "POST",
"enabled": true,
"enabled_logs": true,
"player_context_enabled": true,
}
data, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.aghanim.com/s2s/v1/webhooks", bytes.NewBuffer(data))
req.Header.Set("Authorization", "Bearer <YOUR_S2S_KEY>")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
var webhook map[string]interface{}
json.NewDecoder(resp.Body).Decode(&webhook)
return webhook
}
Test your integration
Pass mock to init() to drive a full purchase in tests without a network or a real payment. See Testing with mock mode for the option and every scenario.
After you have handled the webhooks, check that the purchased items are in your inventory. That’s all.
Next steps
- See the JavaScript SDK reference for presentation modes, the full event list, and error codes.
- See Currency codes and minor units to learn how the Aghanim represents monetary amounts.
- See Payment Webhook to learn about the payment progress when the player visits the payment form.
Need help?
Contact our integration team at integration@aghanim.com