Getting Started

The Dodopin Payment API allows you to embed a secure, hosted checkout experience into your platform. Your server generates a signed checkout session, redirects the end-user to the hosted payment page, and receives a cryptographically signed webhook notification upon payment completion — no card data ever touches your infrastructure.

Credentials

Public
API Key
Identifies your store in all API requests and outbound webhook deliveries.
Secret
API Secret
HMAC-SHA256 signing key used in both Create Session requests and webhook signature verification. Never expose this in client-side code or request bodies.
Public
Store ID
Your numeric store identifier, required in every session request.
Config
Webhook URL
Your HTTPS endpoint that receives IPN notifications. Must respond with OK.
Config
Token TTL
Generated checkout tokens are valid for 2 hours. Redirect the user immediately after creation.
Find your credentials in Store Settings → Integration. Contact your account manager if access is restricted.

Integration Flow

  • 1 Generate session — From your server, POST customer data + HMAC-SHA256 hash to /checkout/token.php. On success you receive a signed token and a redirect URL. The token is valid for 2 hours.
  • 2 Redirect user — Immediately forward the end-user's browser to the redirect URL returned in step 1. Do not store or reuse the token.
  • 3 Hosted checkout — The customer selects a product and completes payment on the Dodopin hosted page. No sensitive payment data touches your infrastructure.
  • 4 Receive IPN webhook — After successful payment, Dodopin sends a signed HTTP POST to your registered Webhook URL. Verify the hash signature, check status === "success", and fulfill the order using order_ref. Respond with HTTP 200 and body OK.
Server-side only. Never generate the hash or expose api_secret in front-end JavaScript or mobile app code. All requests to /checkout/token.php must originate from your backend server.

Authentication

The API uses HMAC-SHA256 signatures in two places: (1) you sign every Create Session request so your api_secret never travels over the wire, and (2) the platform signs every webhook it dispatches so you can verify the notification is genuine.

Request Hash (Create Session)

  hash_string =
 api_key
+ "|" +store_id
+ "|" +user_id
+ "|" +username
+ "|" +user_email
hash = base64_encode( HMAC_SHA256( hash_string, api_secret ))
Concatenate fields with a pipe | delimiter between each field, in the exact order shown. Pass the raw binary HMAC digest to base64_encode — do not hex-encode first. Your api_secret is the signing key and must never be sent in the request body.

Webhook Signature Formula

  hash_string =
 merchant_id
+order_ref
+user_fullname
+invoice_mail
+gateway_name
+status
+api_key
hash = base64_encode( HMAC_SHA256( hash_string, api_secret ))

Webhook Verification Example

$API_KEY    = 'YOUR_API_KEY';
$API_SECRET = 'YOUR_API_SECRET';

// All fields come directly from the flat POST body
$hash_string =
    ($_POST['merchant_id']  ?? '') .
    ($_POST['order_ref']    ?? '') .
    ($_POST['user_fullname'] ?? '') .
    ($_POST['invoice_mail'] ?? '') .
    ($_POST['gateway_name'] ?? '') .
    ($_POST['status']       ?? '') .
    $API_KEY;

$expected = base64_encode(
    hash_hmac('sha256', $hash_string, $API_SECRET, true)
);

// hash_equals() prevents timing attacks
if (!hash_equals($expected, $_POST['hash'] ?? '')) {
    http_response_code(403);
    die('invalid_hash');
}
const crypto = require('crypto');

// All fields come directly from the flat POST body
const hashString =
    String(req.body.merchant_id  ?? '') +
    String(req.body.order_ref    ?? '') +
    String(req.body.user_fullname ?? '') +
    String(req.body.invoice_mail ?? '') +
    String(req.body.gateway_name ?? '') +
    String(req.body.status       ?? '') +
    API_KEY;

const expected = Buffer
    .from(crypto.createHmac('sha256', API_SECRET).update(hashString).digest())
    .toString('base64');

// timingSafeEqual prevents timing attacks
const a = Buffer.from(req.body.hash ?? '');
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(403).send('invalid_hash');
}
import hmac, hashlib, base64
from flask import request

API_KEY    = 'YOUR_API_KEY'
API_SECRET = 'YOUR_API_SECRET'

# All fields come directly from the flat POST body
f = lambda k: request.form.get(k, '')
hash_string = (
    f('merchant_id')  +
    f('order_ref')    +
    f('user_fullname') +
    f('invoice_mail') +
    f('gateway_name') +
    f('status')       +
    API_KEY
)

expected = base64.b64encode(
    hmac.new(API_SECRET.encode(), hash_string.encode(), hashlib.sha256).digest()
).decode()

# compare_digest prevents timing attacks
if not hmac.compare_digest(f('hash'), expected):
    abort(403)

Create Checkout Session

POST your credentials and customer context to the token endpoint from your server. On success, the API returns a signed, single-use redirect URL. Forward the end-user's browser to this URL to begin the hosted payment flow.

POST https://dodopin.com/checkout/token.php Content-Type: form-urlencoded → JSON

Code Examples

<?php
$API_KEY    = 'YOUR_API_KEY';
$API_SECRET = 'YOUR_API_SECRET';
$store_id   = 12345;
$user_id    = 678;
$username   = 'username';
$user_email = '[email protected]';

$hash = base64_encode(
    hash_hmac('sha256', $API_KEY . '|' . $store_id . '|' . $user_id . '|' . $username . '|' . $user_email, $API_SECRET, true)
);

$post_data = [
    'api_key'       => $API_KEY,
    'hash'          => $hash,
    'store_id'      => $store_id,
    'user_id'       => $user_id,
    'username'      => $username,
    'user_email'    => $user_email,
    'user_ip'       => $_SERVER['REMOTE_ADDR'] ?? '',
    'user_fullname' => 'John Doe',
    'user_phone'    => '+13125550100',
    'lang'          => 'tr',        // page opening language (default: tr)
    'currency'      => 'TRY',       // page opening currency (default: TRY)
];

$ch = curl_init('https://dodopin.com/checkout/token.php');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $post_data,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_TIMEOUT        => 30,
]);

$response = json_decode(curl_exec($ch), true);
curl_close($ch);

if ($response['success'] ?? false) {
    header('Location: ' . $response['redirect']);
    exit;
}

throw new RuntimeException('Token error: ' . ($response['message'] ?? 'unknown'));
?>
const https  = require('https');
const qs     = require('querystring');
const crypto = require('crypto');

const API_KEY    = 'YOUR_API_KEY';
const API_SECRET = 'YOUR_API_SECRET';
const store_id   = 12345;
const user_id    = 678;
const username   = 'username';
const user_email = '[email protected]';

const hash = Buffer
    .from(crypto.createHmac('sha256', API_SECRET)
        .update(String(API_KEY) + '|' + String(store_id) + '|' + String(user_id) + '|' + username + '|' + user_email)
        .digest())
    .toString('base64');

const body = qs.stringify({
    api_key:       API_KEY,
    hash:          hash,
    store_id:      store_id,
    user_id:       user_id,
    username:      username,
    user_email:    user_email,
    user_ip:       '1.2.3.4',
    user_fullname: 'John Doe',
    user_phone:    '+393331234567',
    lang:          'tr',        // page opening language (default: tr)
    currency:      'TRY',       // page opening currency (default: TRY)
});

const url = new URL('https://dodopin.com/checkout/token.php');

const req = https.request({
    hostname: url.hostname,
    path:     url.pathname,
    method:   'POST',
    headers:  {
        'Content-Type':   'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(body),
    },
}, (res) => {
    let data = '';
    res.on('data', c => data += c);
    res.on('end',  () => {
        const json = JSON.parse(data);
        if (json.success) console.log('Redirect →', json.redirect);
        else throw new Error(json.message);
    });
});

req.on('error', err => { throw err; });
req.write(body);
req.end();
import requests, hmac, hashlib, base64

API_KEY    = 'YOUR_API_KEY'
API_SECRET = 'YOUR_API_SECRET'
store_id   = 12345
user_id    = 678
username   = 'username'
user_email = '[email protected]'

hash_string = API_KEY + '|' + str(store_id) + '|' + str(user_id) + '|' + username + '|' + user_email
hash_val = base64.b64encode(
    hmac.new(API_SECRET.encode(), hash_string.encode(), hashlib.sha256).digest()
).decode()

resp = requests.post(
    'https://dodopin.com/checkout/token.php',
    data={
        'api_key':       API_KEY,
        'hash':          hash_val,
        'store_id':      store_id,
        'user_id':       user_id,
        'username':      username,
        'user_email':    user_email,
        'user_ip':       '1.2.3.4',
        'user_fullname': 'John Doe',
        'user_phone':    '+201001234567',
        'lang':          'tr',        # page opening language (default: tr)
        'currency':      'TRY',       # page opening currency (default: TRY)
    },
    timeout=30,
)
resp.raise_for_status()
data = resp.json()

if data.get('success'):
    redirect_url = data['redirect']   # redirect user here
else:
    raise RuntimeError(data.get('message'))
# Compute hash first (shell)
HASH=$(printf '%s' 'YOUR_API_KEY|12345|678|username|[email protected]' \
  | openssl dgst -sha256 -hmac 'YOUR_API_SECRET' -binary | base64)

curl -X POST 'https://dodopin.com/checkout/token.php' \
  -d 'api_key=YOUR_API_KEY' \
  --data-urlencode "hash=$HASH" \
  -d 'store_id=12345' \
  -d 'user_id=678' \
  -d 'username=username' \
  -d '[email protected]' \
  -d 'user_ip=1.2.3.4' \
  -d 'user_fullname=John+Doe' \
  -d 'user_phone=%2B13125550100' \
  -d 'lang=tr' \
  -d 'currency=TRY'

Response

200 — Success
{
  "success": true,
  "token": "eyJzIjoxMjM0NX0...",
  "redirect": "https://dodopin.com/tr/store/your-store-slug?token=eyJzIjoxMjM0NX0...&cur=TRY"
}
200 — Error
{
  "success": false,
  "message": "Invalid API credentials or store is not active."
}

Request Parameters

Field Type Description Validation Required
api_key String Your store API key, found in Store Settings → Integration. Non-empty string required
hash String HMAC-SHA256 request signature. See Authentication for the formula. Your api_secret is the signing key — never send it in the request body. base64_encode(HMAC-SHA256(api_key|store_id|user_id|username|user_email, api_secret)) — fields joined with pipe | required
store_id Integer Numeric ID of your store, found in Store Settings → Integration. Positive integer required
user_id String / Int The customer's unique identifier on your platform. Used to identify the buyer in webhook notifications. Max 64 chars. Control characters are stripped. required
username String The customer's username as it appears on your platform. Displayed on the checkout page. Letters, digits, _ and - only. Max 64 chars. Spaces and special characters are rejected. required
user_email String The customer's e-mail address. Used for the payment invoice. Must be a valid e-mail address (RFC 5321). required
user_ip String The end-user's IP address. Pass the real client IP, not your server IP. Must be a valid IPv4 or IPv6 address. required
user_fullname String The customer's full name (first and last name). Used on the payment invoice. Max 128 chars. Control characters are stripped. required
user_phone String The customer's phone number including country code. Digits and + only. Max 20 chars. Must contain at least one digit
(e.g. +13125550100, +393331234567, +201001234567).
required
lang String Page opening language. Default: tr tr, en optional
currency String Page opening currency. Default: TRY TRY, USD, EUR optional

Webhooks (IPN)

After each successful payment, the platform dispatches an HTTP POST to your registered Webhook URL with signed order, customer, and payment data. Your endpoint must verify the hash signature, fulfill the order when status = success, and respond with HTTP 200 and body OK. Non-compliant responses trigger automatic retries.

Register your Webhook URL in Store Settings → Integration. The endpoint must be publicly reachable, TLS-secured, and respond within 15 seconds. Webhooks are only dispatched on successful payments. On failure, the system retries up to 100 times, once per minute via a background cron job.

Handler Implementation

<?php
$API_KEY    = 'YOUR_API_KEY';
$API_SECRET = 'YOUR_API_SECRET';

// Payload is a flat POST body — all fields at root level
$merchant_id  = $_POST['merchant_id']  ?? '';
$order_ref    = $_POST['order_ref']    ?? '';
$user_fullname = $_POST['user_fullname'] ?? '';
$invoice_mail = $_POST['invoice_mail'] ?? '';
$gateway_name = $_POST['gateway_name'] ?? '';
$status       = $_POST['status']       ?? '';
$received_hash = $_POST['hash']        ?? '';

$expected = base64_encode(hash_hmac('sha256',
    $merchant_id . $order_ref . $user_fullname .
    $invoice_mail . $gateway_name . $status . $API_KEY,
    $API_SECRET, true
));

if (!hash_equals($expected, $received_hash)) {
    http_response_code(403);
    die('invalid_hash');
}

if ($status === 'success') {
    $topup = (float) ($_POST['total_topup_amount'] ?? '0');
    $user  = $_POST['username'] ?? '';
    // Credit $topup to the player's in-game balance
    // e.g. addBalance($user, $topup);
}

header('Content-Type: text/plain');
echo 'OK';
?>
const express = require('express');
const crypto  = require('crypto');

const app = express();
app.use(express.urlencoded({ extended: true }));

app.post('/webhook', (req, res) => {
    // Payload is a flat POST body — all fields at root level
    const {
        merchant_id  = '',
        order_ref    = '',
        user_fullname = '',
        invoice_mail = '',
        gateway_name = '',
        status       = '',
        hash         = '',
    } = req.body;

    const expected = Buffer.from(crypto
        .createHmac('sha256', process.env.API_SECRET)
        .update(merchant_id + order_ref + user_fullname + invoice_mail + gateway_name + status + process.env.API_KEY)
        .digest()
    ).toString('base64');

    const hBuf = Buffer.from(hash);
    const eBuf = Buffer.from(expected);
    if (hBuf.length !== eBuf.length || !crypto.timingSafeEqual(hBuf, eBuf))
        return res.status(403).send('invalid_hash');

    if (status === 'success') {
        const topup = parseFloat(req.body.total_topup_amount || '0');
        const user  = req.body.username || '';
        // Credit topup to the player's in-game balance
        // e.g. addBalance(user, topup);
    }

    res.type('text').send('OK');
});

app.listen(3000);
import hmac, hashlib, base64
from flask import Flask, request, abort

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    # Payload is a flat POST body — all fields at root level
    f = lambda k: request.form.get(k, '')
    hash_string = (
        f('merchant_id')  +
        f('order_ref')    +
        f('user_fullname') +
        f('invoice_mail') +
        f('gateway_name') +
        f('status')       +
        API_KEY
    )
    expected = base64.b64encode(
        hmac.new(API_SECRET.encode(), hash_string.encode(), hashlib.sha256).digest()
    ).decode()

    if not hmac.compare_digest(f('hash'), expected):
        abort(403)

    if f('status') == 'success':
        topup = float(f('total_topup_amount') or '0')
        user  = f('username')
        # Credit topup to the player's in-game balance
        # e.g. add_balance(user, topup)

    return 'OK', 200, {'Content-Type': 'text/plain'}

Payload Fields

FieldTypeDescriptionIn Hash
merchant_idStringYour Store / Merchant IDyes — pos 1
order_refStringUnique order reference generated by Dodopinyes — pos 2
user_fullnameStringCustomer full nameyes — pos 3
invoice_mailStringCustomer e-mail addressyes — pos 4
gateway_nameStringPayment gateway identifier (e.g. stripe, cryptomus, iyzico)yes — pos 5
statusStringAlways success for automatic webhooksyes — pos 6
api_keyStringYour store API key — not sent in the payload, but appended to the hash string as position 7. Use your stored api_key when verifying the signature.yes — pos 7
user_phoneStringCustomer phone number with country codeno
product_idIntegerProduct ID — omitted if not applicable to the orderno
product_nameStringProduct title as stored at checkout (product_name_snapshot) — omitted if emptyno
quantityStringOrder quantity (min 1)no
product_topup_amountStringUnit topup/credit amount per product (e.g. 1.00)no
total_topup_amountStringTotal credit to add to the user's account. Equals quantity × product_topup_amount (e.g. 250.00). Use this value to top up / credit the buyer in your game or platform.no
product_currencyStringProduct currency code (e.g. TRY, USD, EUR)no
unit_priceStringUnit price per product (e.g. 49.90)no
total_priceStringTotal order price: quantity × unit_price (e.g. 149.70)no
net_merchant_earningStringNet merchant earning after commission deduction (e.g. 127.25)no
usernameStringGame / account username when available (mirrors checkout token field username)no
hashStringHMAC-SHA256 base64 signature — always verify before processing
Crediting the player. After verifying the webhook signature, read the total_topup_amount field to determine how much credit/balance to add to the user identified by username (or user_id from your original token request). This is the definitive amount the customer purchased — apply it directly to the player's in-game wallet or balance.

Error Codes

The token endpoint always returns HTTP 200. Evaluate the success boolean to determine the outcome. Every error response includes a message string describing the failure.

Webhook signature mismatches should be rejected with HTTP 403 and body invalid_hash — do not process these notifications.

Error Reference

ScenarioHTTPsuccessmessageCommon Cause
Any required field is empty or missing 200 false Missing required field: <field_name>. Field not included in POST body
username contains invalid characters 200 false Invalid value for username. Spaces, Turkish chars, or symbols in username. Only a-z A-Z 0-9 _ - allowed.
user_email is not a valid address 200 false Invalid value for user_email. Malformed e-mail address
user_ip is not a valid IP address 200 false Invalid value for user_ip. Sending server IP instead of client IP, or invalid format
Wrong api_key / store_id combination, or store is inactive 200 false Invalid API credentials or store is not active. api_key does not match store_id, or store status is not approved_admin
Hash signature mismatch 200 false Invalid hash. Please recalculate… Wrong field order, wrong api_secret, or missing pipe delimiter. Fields must be joined with | (pipe) in the exact order shown.
Webhook signature mismatch 403 Plain text: invalid_hash Your endpoint rejected the webhook due to a bad signature — do not process these

Rate Limits

The token endpoint enforces three independent rate-limit layers. All layers must pass before a session is created. Exceeding any returns HTTP 429 with a Retry-After header indicating when to retry.

Limit Layers

Layer Scope Default Limit Window HTTP
Fast IP Limit Per client IP address 60 requests 60 seconds 429
Token IP Limit Per client IP address 30 requests 1 minute 429
Token API Key Limit Per api_key 60 requests 1 minute 429
Limits shown are defaults and may be tuned per-store by the platform administrator. Server-to-server calls from your backend count against the client IP layer — ensure your backend's outbound IP is consistent and not shared across many services.

Best Practices

  • 1 Create sessions on demand — only generate a token when the user is actively about to pay. Never pre-generate tokens in batch.
  • 2 Respect Retry-After — read the header value and delay your retry exactly that many seconds. Immediate retries will continue to be rejected and waste your quota.
  • 3 Do not cache and reuse tokens — tokens are single-use and expire after 2 hours. Reusing or sharing tokens across users is not supported.
  • 4 Use exponential back-off — if you receive a 429, implement exponential back-off with jitter to avoid thundering-herd retries.

API Reference

Technical summary of endpoints, methods, response formats, and security requirements.

Token Endpoint
URL /checkout/token.php
Method POST
Content-Type form-urlencoded
Response JSON
Version 1.0
Security
Webhook Signature HMAC-SHA256 + Base64
Token TTL 2 hours
Rate Limit (IP) 60 req / 60 s & 30 req / 1 min
Rate Limit (API Key) 60 req / 1 min
Rate Limit Error 429 + Retry-After
Webhooks
Trigger Successful payments only
Max Attempts 100 attempts per payment
Cron Retry Every 1 minute for failed deliveries
Timeout 15 seconds
Required Response OK 200 text/plain