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.
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.
- 1Integration nameThe title / brand of your integration
- 2Whitelist IPsServer IP(s) that will call the API
- 3Callback URLWhere we POST signed status callbacks
- 4EmailContact address for keys and notifications
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.
Step ③ depends on the payment_type EkstraPAY chose (by your amount-range routing):
Redirect the customer to the bank's 3D verification page.
Redirect the customer to our hosted card form (MOTO).
Send the payment link to the customer by email or SMS.
When kyc_required is set, the customer uploads documents with a 6-digit code.
Base URL & Response Envelope
| Environment | Base URL |
|---|---|
| Production | https://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.
| Header | Value |
|---|---|
X-API-Key | Your API key |
X-Timestamp | Unix seconds. Within ±300s of server time. |
X-Nonce | Unique per request (e.g. 32 hex). Single-use (600s replay window). |
X-Signature | HMAC-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 signedSDK Download & Install
Dependency-free clients. Each package (ZIP) contains the SDK, a runnable
example and a README. Single-file downloads are also available.
Postman collection
This is the merchant integration API. The admin panel and hosted payment / KYC screens have their own separate documentation and collections.
Verify a download
shasum -a 256 ekstrapay_cc.py # compare with the checksum aboveInstall
# 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
| Method | Parameters | Returns / Raises |
|---|---|---|
create_payment / createPayment | body object (amount, order_id, …) | the response data object · raises on error |
check_payment / checkPayment | transaction_id, sync=false | status data |
cancel_payment / cancelPayment | transaction_id | data |
detail_payment / detailPayment | transaction_id | data |
verify_callback / verifyCallback static | callback 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
| Field | Type | Req. | Notes |
|---|---|---|---|
amount | number | ✔ | > 0, within the API's min/max limits |
order_id | string | ✔ | Your reference; unique per API service |
customer_ref | string | ✔ | Your customer id (blocking / history) |
first_name | string | ✔ | Customer profile base field |
last_name | string | ✔ | Customer profile base field |
email | string | ✔ | 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) |
phone | string | ✔ | 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_date | date | ✔ | YYYY-MM-DD — never send an empty string |
tc_no | string | ✔ | National id (groups customer & KYC) |
birth_place | string | ✔ | Customer profile field |
address | string | ✔ | Customer profile field |
city | string | ✔ | Customer profile field |
zip_code | string | ✔ | Customer profile field |
return_url_success | url | ✔ | Where the customer returns after a successful payment |
return_url_fail | url | ✔ | 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
| type | What you do with the response |
|---|---|
3d | Redirect the customer to data.redirect_url. Result arrives via callback. |
native | Redirect to data.pay_screen.pay_url (our hosted card form, MOTO). |
link | Send 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 hereKYC 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 400Check · 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 # acknowledgerequire '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); // acknowledgeconst { 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 TAMPEREDSandbox & Test Cards
Assign the SandboxCC module to a test API service (no real provider) and use:
| Card | Expiry / CVV | Result |
|---|---|---|
4111 1111 1111 1111 | any future / any | Approved |
5555 5555 5555 4444 | any future / any | Approved |
4000 0000 0000 0002 | any future / any | Declined |
4000 0000 0000 3220 | any future / any | 3D fallback |
KYC test: amount ending in .99 (e.g. 100.99) → kyc_required=true.
Enums & Statuses
Transaction status
| value | label | |
|---|---|---|
CC_NEW | Yeni | new |
CC_PROCESSING | İşlemde | pending |
CC_REDIRECT | 3D Yönlendirme | 3D |
CC_KYC_REQUIRED | KYC Bekleniyor | kyc |
CC_APPROVED | Onaylandı | final ✓ |
CC_REJECTED | Reddedildi | final |
CC_CANCELLED | İptal Edildi | final |
CC_TIMEOUT | Zaman Aşımı | final |
CC_CHARGEBACK | Chargeback | dispute |
CC_REFUNDED | İade Edildi | refund |
payment_type
3d · 3D Secure (redirect_url) | native · hosted screen | link · payment link |
Error Codes
Business errors return status:"error" with data.error_code. Handle the code, not the message.
| error_code | HTTP | Meaning |
|---|---|---|
CC-AUTH-001 | 401 | Missing required headers |
CC-AUTH-002 | 401 | Timestamp outside ±300s |
CC-AUTH-003 | 401 | Nonce replay |
CC-AUTH-004 | 401 | Invalid API key |
CC-AUTH-005 | 401 | Signature mismatch |
CC-API-001 | 401 | API service not active |
CC-API-002 | 401 | IP not whitelisted |
CC-API-003 | 400 | Amount outside min/max |
CC-API-004 | 400 | order_id already used |
CC-API-005 | 400 | New transactions disabled |
CC-BLOCK-001 | 403 | Customer blocked |
CC-POOL-001 | 400 | No module range for amount |
CC-POOL-002 | 400 | No active module in range |
CC-MODULE-001 | 400 | Provider declined |
CC-TX-001 | 400 | Transaction not found |
CC-TX-002 | 400 | Finalized, cannot cancel |
CC-VALIDATION | 422 | Field validation (no error_code) |
EkstraPAY Credit Card API · v1