Credit Card API & SDK

Integrate card payments in minutes: 3D-Secure, non-3D (MOTO / native), hosted payment screen, KYC, chargebacks and signed callbacks. Official SDKs for Python, PHP and Node.js handle all request signing for you.

HMAC-SHA256 signed 3 official SDKs 0 dependencies Signed callbacks REST · JSON

Getting started

To open your integration, send us the details below. We create your API service and return your API key and API token, after which every endpoint on this page becomes available.

Overview

A payment flows between three parties: your server, EkstraPAY and the customer's browser. Your server signs the request, EkstraPAY returns where to send the customer, and the final result comes back to you as a signed callback.

Your Server merchant backend EkstraPAY Credit Card API Customer browser ① create · check · cancel · detail (HMAC signed) ② response: redirect_url · pay_screen.pay_url · kyc.code ③ redirect the customer (based on payment_type / KYC) ④ pay on hosted screen / 3D page · upload KYC docs FINAL RESULT ⑤ callback → your callback_url (final status + verifiable hash) retried on failure · verify with EkstraPayCC.verifyCallback()

Step ③ depends on the payment_type EkstraPAY chose (by your amount-range routing):

3d3D Secure

Redirect the customer to the bank's 3D verification page.

→data.redirect_url
nativeHosted form

Redirect the customer to our hosted card form (MOTO).

→data.pay_screen.pay_url
linkPayment link

Send the payment link to the customer by email or SMS.

→data.pay_screen.pay_url
kycKYC step

When kyc_required is set, the customer uploads documents with a 6-digit code.

→data.kyc.code

Base URL & Response Envelope

EnvironmentBase URL
Productionhttps://v2.ekstrapay.com/api

Every response uses one envelope. On success read data; on error read description and data.error_code.

{
  "status": "success",             // or "error"
  "title": "Başarılı",
  "description": "İşlem oluşturuldu",
  "data": { ... },                 // payload, or { "error_code": "CC-..." } on errors
  "timestamp": "2026-08-16T12:00:00+00:00"
}

Authentication (HMAC-SHA256)

Signed endpoints require four headers. The SDKs build these automatically. You only need the algorithm below if you implement signing yourself.

HeaderValue
X-API-KeyYour API key
X-TimestampUnix seconds. Within ±300s of server time.
X-NonceUnique per request (e.g. 32 hex). Single-use (600s replay window).
X-SignatureHMAC-SHA256 hex (below)
cleaned   = body with strings trimmed and empty("")/null values removed
json      = compact JSON of cleaned (no spaces; unicode & slashes NOT escaped);  empty -> "[]"
message   = apiKey + "|" + timestamp + "|" + nonce + "|" + json
signature = hex( HMAC_SHA256(message, apiToken) )
BODY sent = json          # send the exact canonical string you signed
Why "canonical": the server re-cleans & re-encodes the received body before verifying, so the string you sign must equal what the server rebuilds: compact, no empty/null fields, keys in send order. Every SDK sends exactly the signed string, so this is handled for you.

SDK Download & Install

Dependency-free clients. Each package (ZIP) contains the SDK, a runnable example and a README. Single-file downloads are also available.

🐍
PythonPython 3.6+ · stdlib only Download package (ZIP) single file: ekstrapay_cc.py
sha256: afb7f343…164394d4
🐘
PHPPHP 7.4+ · curl, json Download package (ZIP) single file: EkstraPayCC.php
sha256: c533dc1d…9d7a5520
⬢
Node.jsNode 12+ · built-ins Download package (ZIP) single file: ekstrapay-cc.js
sha256: 5e184959…2004283f

Postman collection

This is the merchant integration API. The admin panel and hosted payment / KYC screens have their own separate documentation and collections.

Integration · Postman collection (JSON) Everything · SDKs + examples + Postman (ZIP)

Verify a download

shasum -a 256 ekstrapay_cc.py     # compare with the checksum above

Install

# No package needed. Place the file in your project and import it:
#   project/ekstrapay_cc.py
from ekstrapay_cc import EkstraPayCC, EkstraPayError
// Place the file in your project and require it:
require __DIR__ . '/EkstraPayCC.php';   // classes: EkstraPayCC, EkstraPayException
// Place the file in your project and require it:
const { EkstraPayCC, EkstraPayError } = require('./ekstrapay-cc');
// (ESM: import EkstraPayCC from './ekstrapay-cc.js' after adding module.exports interop)

Initialize the client

cc = EkstraPayCC(
    api_key="YOUR_API_KEY",
    api_token="YOUR_API_TOKEN",
    base_url="https://v2.ekstrapay.com/api",   # optional
    timeout=30,                                 # optional (seconds)
)
$cc = new EkstraPayCC(
    'YOUR_API_KEY',
    'YOUR_API_TOKEN',
    'https://v2.ekstrapay.com/api',   // optional
    30                                // optional timeout (seconds)
);
const cc = new EkstraPayCC({
  apiKey: 'YOUR_API_KEY',
  apiToken: 'YOUR_API_TOKEN',
  baseUrl: 'https://v2.ekstrapay.com/api',   // optional
  timeout: 30000,                            // optional (ms)
});

Method Reference

MethodParametersReturns / Raises
create_payment / createPaymentbody object (amount, order_id, …)the response data object · raises on error
check_payment / checkPaymenttransaction_id, sync=falsestatus data
cancel_payment / cancelPaymenttransaction_iddata
detail_payment / detailPaymenttransaction_iddata
verify_callback / verifyCallback staticcallback payload (dict/array/object)true if the hash is authentic

On any API/business error the SDK raises EkstraPayError / EkstraPayException carrying .error_code (e.g. "CC-API-003"), .http_code and .data.

Usage: Create a payment (full flow)

Create the transaction, then send the customer to the right place based on payment_type.

from ekstrapay_cc import EkstraPayCC, EkstraPayError

cc = EkstraPayCC("YOUR_API_KEY", "YOUR_API_TOKEN")

try:
    res = cc.create_payment(
        amount=250.75,
        order_id="ORD-1001",
        customer_ref="cust-123",
        tc_no="11111111111",
        first_name="Ali", last_name="Veli",
        email="[email protected]", phone="+905321234567",
        birth_date="1990-06-20", birth_place="Istanbul",
        address="Ornek Mah. No:1", city="Istanbul", zip_code="34000",
        return_url_success="https://merchant.com/ok",
        return_url_fail="https://merchant.com/fail",
    )

    pt = res["payment_type"]
    if pt == "3d":
        redirect_to(res["redirect_url"])                 # bank 3D page
    elif pt in ("native", "link"):
        redirect_to(res["pay_screen"]["pay_url"])        # our hosted screen

    if res.get("kyc"):
        show_kyc_code(res["kyc"]["code"])                # 6-digit KYC code

except EkstraPayError as e:
    print(e.error_code, "-", e.message)                  # e.g. CC-API-003 - Tutar ...
require 'EkstraPayCC.php';

$cc = new EkstraPayCC('YOUR_API_KEY', 'YOUR_API_TOKEN');

try {
    $res = $cc->createPayment([
        'amount'             => 250.75,
        'order_id'           => 'ORD-1001',
        'customer_ref'       => 'cust-123',
        'tc_no'              => '11111111111',
        'first_name'         => 'Ali',
        'last_name'          => 'Veli',
        'email'              => '[email protected]',
        'phone'              => '+905321234567',
        'birth_date'         => '1990-06-20',
        'birth_place'        => 'Istanbul',
        'address'            => 'Ornek Mah. No:1',
        'city'               => 'Istanbul',
        'zip_code'           => '34000',
        'return_url_success' => 'https://merchant.com/ok',
        'return_url_fail'    => 'https://merchant.com/fail',
    ]);

    if ($res['payment_type'] === '3d') {
        header('Location: ' . $res['redirect_url']);            // bank 3D page
    } else { // native | link
        header('Location: ' . $res['pay_screen']['pay_url']);   // hosted screen
    }

    if (!empty($res['kyc'])) {
        $code = $res['kyc']['code'];                            // 6-digit KYC code
    }
} catch (EkstraPayException $e) {
    error_log($e->errorCode . ' - ' . $e->getMessage());        // e.g. CC-API-004 - ...
}
const { EkstraPayCC, EkstraPayError } = require('./ekstrapay-cc');

const cc = new EkstraPayCC({ apiKey: 'YOUR_API_KEY', apiToken: 'YOUR_API_TOKEN' });

try {
  const res = await cc.createPayment({
    amount: 250.75,
    order_id: 'ORD-1001',
    customer_ref: 'cust-123',
    tc_no: '11111111111',
    first_name: 'Ali', last_name: 'Veli',
    email: '[email protected]', phone: '+905321234567',
    birth_date: '1990-06-20', birth_place: 'Istanbul',
    address: 'Ornek Mah. No:1', city: 'Istanbul', zip_code: '34000',
    return_url_success: 'https://merchant.com/ok',
    return_url_fail: 'https://merchant.com/fail',
  });

  if (res.payment_type === '3d') {
    res_redirect(res.redirect_url);                 // bank 3D page
  } else {                                          // native | link
    res_redirect(res.pay_screen.pay_url);           // hosted screen
  }

  if (res.kyc) showKycCode(res.kyc.code);           // 6-digit KYC code

} catch (e) {
  console.error(e.errorCode, '-', e.message);       // e.g. CC-BLOCK-001 - ...
}

Check status · Cancel · Detail

st = cc.check_payment("550e8400-...")          # {'status': {...}, 'amount': '250.75', ...}
st = cc.check_payment("550e8400-...", sync=True)  # also queries provider (provider_status)
cc.cancel_payment("550e8400-...")              # cancel a non-finalized tx
det = cc.detail_payment("550e8400-...")        # full detail
$st  = $cc->checkPayment('550e8400-...');          // ['status' => [...], 'amount' => '250.75', ...]
$st  = $cc->checkPayment('550e8400-...', true);    // sync with provider
$cc->cancelPayment('550e8400-...');
$det = $cc->detailPayment('550e8400-...');
const st  = await cc.checkPayment('550e8400-...');        // { status:{...}, amount:'250.75', ... }
const st2 = await cc.checkPayment('550e8400-...', true);  // sync with provider
await cc.cancelPayment('550e8400-...');
const det = await cc.detailPayment('550e8400-...');

Error Handling

Every error is an exception with a stable error_code. Branch on the code, not the message.

try:
    res = cc.create_payment(amount=5, order_id="ORD-9")
except EkstraPayError as e:
    if e.error_code == "CC-API-003":
        ...  # amount out of range
    elif e.error_code == "CC-BLOCK-001":
        ...  # customer blocked
    elif e.error_code and e.error_code.startswith("CC-AUTH"):
        ...  # signing/credential problem
    else:
        log(e.http_code, e.error_code, e.message, e.data)
try {
    $res = $cc->createPayment(['amount' => 5, 'order_id' => 'ORD-9']);
} catch (EkstraPayException $e) {
    switch ($e->errorCode) {
        case 'CC-API-003': /* amount out of range */ break;
        case 'CC-BLOCK-001': /* customer blocked */ break;
        default: error_log("{$e->httpCode} {$e->errorCode} {$e->getMessage()}");
    }
}
try {
  await cc.createPayment({ amount: 5, order_id: 'ORD-9' });
} catch (e) {
  switch (e.errorCode) {
    case 'CC-API-003': /* amount out of range */ break;
    case 'CC-BLOCK-001': /* customer blocked */ break;
    default: console.error(e.httpCode, e.errorCode, e.message, e.data);
  }
}

Raw curl (manual HMAC)

For quick tests without an SDK. BODY must be the canonical string (compact, no empty/null fields):

API_KEY="YOUR_API_KEY"
API_TOKEN="YOUR_API_TOKEN"
BODY='{"amount":250.75,"order_id":"ORD-1001","customer_ref":"cust-123","tc_no":"11111111111"}'

TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
SIG=$(printf '%s' "$API_KEY|$TS|$NONCE|$BODY" | openssl dgst -sha256 -hmac "$API_TOKEN" | sed 's/^.* //')

curl -sS https://v2.ekstrapay.com/api/v1/cc/payment/create \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -H "X-API-Key: $API_KEY" -H "X-Timestamp: $TS" -H "X-Nonce: $NONCE" -H "X-Signature: $SIG" \
  -d "$BODY"

Create Payment

POST/v1/cc/payment/create · signed

FieldTypeReq.Notes
amountnumber✔> 0, within the API's min/max limits
order_idstring✔Your reference; unique per API service
customer_refstring✔Your customer id (blocking / history)
first_namestring✔Customer profile base field
last_namestring✔Customer profile base field
emailstring✔Required — a random/placeholder value is accepted if you do not collect it, but it must stay unique and stable per customer (used to match returning customers)
phonestring✔Required, E.164 with country code (e.g. +905321234567) — a random value is accepted if you do not collect it, but it must stay unique and stable per customer
birth_datedate✔YYYY-MM-DD — never send an empty string
tc_nostring✔National id (groups customer & KYC)
birth_placestring✔Customer profile field
addressstring✔Customer profile field
citystring✔Customer profile field
zip_codestring✔Customer profile field
return_url_successurl✔Where the customer returns after a successful payment
return_url_failurl✔Where the customer returns after a failed payment

Response:native / link

{ "data": {
  "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
  "order_id": "ORD-1001",
  "payment_type": "native",
  "status": { "value": "CC_PROCESSING", "label": "İşlemde" },
  "redirect_url": null,
  "pay_screen": { "pay_token": "9f8e...", "pay_url": "https://pay.your-frontend.com/cc/pay/9f8e..." },
  "kyc": null
} }

Response:3D

{ "data": { "transaction_id": "550e...", "payment_type": "3d",
  "status": { "value": "CC_REDIRECT", "label": "3D Yönlendirme" },
  "redirect_url": "https://acs.bank.example/3ds/...", "pay_screen": null, "kyc": null } }

Response:KYC required

{ "data": { "status": { "value": "CC_KYC_REQUIRED", "label": "KYC Bekleniyor" },
  "pay_screen": { "pay_token": "...", "pay_url": "..." },
  "kyc": { "code": "483920",
    "required_documents": [ { "key": "id_front", "title": "Kimlik Ön Yüz", "type": "image", "required": true } ],
    "expires_at": "2026-08-17 12:00:00" } } }

Payment Types

typeWhat you do with the response
3dRedirect the customer to data.redirect_url. Result arrives via callback.
nativeRedirect to data.pay_screen.pay_url (our hosted card form, MOTO).
linkSend data.pay_screen.pay_url to the customer as a link.

Hosted Payment Screen (public)

Used by the customer's browser (no signing, rate-limited). You normally don't call these server-side.

POST/v1/cc/pay/session · { pay_token } → amount, merchant, branding, kyc, return_urls

POST/v1/cc/pay/submit

// body
{ "pay_token":"9f8e...", "card_number":"4111111111111111",
  "expiry_month":12, "expiry_year":2030, "cvv":"123", "card_holder":"ALI VELI" }
// response
{ "data": { "status": { "value":"CC_APPROVED" },
            "redirect_url": null,                       // if set -> 3D fallback, go there
            "return_url": "https://merchant.com/ok" } } // else send the customer here
Card data is sent only here, over HTTPS, and is never stored or logged (only a masked PAN). CVV is never stored.

KYC Flow (public, 6-digit code)

POST /v1/cc/kyc/verify   { "code":"483920" }
  → { required_documents:[...], uploaded_documents:[...], status:{...} }
POST /v1/cc/kyc/upload   { "code":"483920", "document_key":"id_front",
                           "content":"data:image/jpeg;base64,...", "mime_type":"image/jpeg" }
POST /v1/cc/kyc/submit   { "code":"483920" }   // all required docs must be uploaded, else 400

Check · Cancel · Detail

POST/v1/cc/payment/check · signed. Returns the current status (sync:true also queries the provider).

POST/v1/cc/payment/cancel · signed. Cancels a non-finalized tx (a finalized tx returns CC-TX-002).

POST/v1/cc/payment/detail · signed. Returns the full transaction record.

Callbacks & Hash Verification

When a transaction finalizes (or changes state) we POST JSON to your callback_url. Failed deliveries are retried. Always verify the hash before trusting a callback.

Callback body

POST {your callback_url}
{
  "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
  "order_id": "ORD-1001",
  "cc_api_id": 1,
  "payment_type": "native",
  "customer_ref": "cust-123",
  "customer_username": "aliveli",
  "tc_no": "11111111111",
  "amount": "250.75",            // 2-decimal string
  "final_amount": "250.75",      // 2-decimal string, or null
  "currency": "TRY",
  "status": "CC_APPROVED",
  "provider_ref": "SBX-1A2B3C4D5E6F",
  "kyc_required": false,
  "hash": "a5d28361e230ce8587621befff44c2bee1d92808e7b775e492225e79f4725887",
  "created_at": "2026-08-16 12:00:00"
}

How the hash is computed

SHA-256 of these fields, joined by : in exactly this order. Missing/null optional fields become an empty string (positions preserved):

hash = sha256(
  transaction_id + ":" + order_id + ":" + customer_ref + ":" + tc_no + ":" +
  amount + ":" + final_amount + ":" + status
)
// amount / final_amount are the 2-decimal strings from the payload (final_amount may be "" when null)

Verify with the SDK (recommended)

from flask import request, abort
from ekstrapay_cc import EkstraPayCC

@app.post("/cc/callback")
def cc_callback():
    payload = request.get_json(force=True)
    if not EkstraPayCC.verify_callback(payload):
        abort(400)                       # forged / tampered
    if payload["status"] == "CC_APPROVED":
        mark_order_paid(payload["order_id"], payload["transaction_id"])
    return "", 200                       # acknowledge
require 'EkstraPayCC.php';

$payload = json_decode(file_get_contents('php://input'), true);

if (!EkstraPayCC::verifyCallback($payload)) {
    http_response_code(400); exit;                 // forged / tampered
}
if ($payload['status'] === 'CC_APPROVED') {
    mark_order_paid($payload['order_id'], $payload['transaction_id']);
}
http_response_code(200);                           // acknowledge
const { EkstraPayCC } = require('./ekstrapay-cc');

app.post('/cc/callback', express.json(), (req, res) => {
  if (!EkstraPayCC.verifyCallback(req.body)) return res.sendStatus(400); // forged
  if (req.body.status === 'CC_APPROVED') {
    markOrderPaid(req.body.order_id, req.body.transaction_id);
  }
  res.sendStatus(200);                                                    // acknowledge
});

Verify manually (no SDK)

# bash: recompute and compare
P='{ ...received JSON... }'
RAW="$txid:$order_id:$customer_ref:$tc_no:$amount:$final_amount:$status"   # null -> ""
CALC=$(printf '%s' "$RAW" | openssl dgst -sha256 | sed 's/^.* //')
[ "$CALC" = "$received_hash" ] && echo OK || echo TAMPERED

Sandbox & Test Cards

Assign the SandboxCC module to a test API service (no real provider) and use:

CardExpiry / CVVResult
4111 1111 1111 1111any future / anyApproved
5555 5555 5555 4444any future / anyApproved
4000 0000 0000 0002any future / anyDeclined
4000 0000 0000 3220any future / any3D fallback

KYC test: amount ending in .99 (e.g. 100.99) → kyc_required=true.

Enums & Statuses

Transaction status

valuelabel
CC_NEWYeninew
CC_PROCESSINGİşlemdepending
CC_REDIRECT3D Yönlendirme3D
CC_KYC_REQUIREDKYC Bekleniyorkyc
CC_APPROVEDOnaylandıfinal ✓
CC_REJECTEDReddedildifinal
CC_CANCELLEDİptal Edildifinal
CC_TIMEOUTZaman Aşımıfinal
CC_CHARGEBACKChargebackdispute
CC_REFUNDEDİade Edildirefund

payment_type

3d · 3D Secure (redirect_url)native · hosted screenlink · payment link

Error Codes

Business errors return status:"error" with data.error_code. Handle the code, not the message.

error_codeHTTPMeaning
CC-AUTH-001401Missing required headers
CC-AUTH-002401Timestamp outside ±300s
CC-AUTH-003401Nonce replay
CC-AUTH-004401Invalid API key
CC-AUTH-005401Signature mismatch
CC-API-001401API service not active
CC-API-002401IP not whitelisted
CC-API-003400Amount outside min/max
CC-API-004400order_id already used
CC-API-005400New transactions disabled
CC-BLOCK-001403Customer blocked
CC-POOL-001400No module range for amount
CC-POOL-002400No active module in range
CC-MODULE-001400Provider declined
CC-TX-001400Transaction not found
CC-TX-002400Finalized, cannot cancel
CC-VALIDATION422Field validation (no error_code)

EkstraPAY Credit Card API · v1