HemenFast — Integration Guide¶
1. Overview¶
HemenFast accepts deposit and withdraw requests from your site, routes them to a payment channel/account, and notifies your callback URL with a signed message once the result is final.
| Direction | Caller | Description |
|---|---|---|
| Request | You → HemenFast | HMAC-signed calls to /paymentapi/* |
| Callback | HemenFast → You | Signed JSON POST to your callback URL on approve/reject |
Deposit flow:
1. User starts a deposit on your site
2. You ──POST /paymentapi/deposit──▶ HemenFast (HMAC signed)
3. HemenFast returns a bank account (bank_name, iban, holder_name)
4. User pays that account
5. On approval HemenFast ──POST {callback_url}──▶ You (hashcode signed)
6. You verify hashcode, credit the user, return HTTP 200
Withdraw is the same shape: create via /paymentapi/withdraw, result delivered by callback.
2. Base URL¶
| Environment | Base URL |
|---|---|
| Production | https://api.hemenfast.com |
A separate test (UAT) URL, if available for your integration, is shared at onboarding. Examples below use the production base URL.
GET /health (no authentication) returns {"status": "healthy", ...} — use it to check connectivity before testing signed requests.
3. Credentials¶
| Value | Description |
|---|---|
| API Key | Sent in the X-API-Key header. Identifies your account. |
| Secret Key | Used to sign requests and to verify the callback hashcode. Never expose it client-side. |
The secret key is shared once at creation. If it is lost it must be regenerated: regeneration issues a new API key and a new secret key together, and the old pair stops working immediately.
4. Authentication (HMAC-SHA256)¶
Every request to /paymentapi/* requires these headers:
| Header | Description | Constraint |
|---|---|---|
X-API-Key |
Your API key | Must be active |
X-Signature |
HMAC-SHA256 signature | 64-char lowercase hex (uppercase hex is rejected) |
X-Timestamp |
Unix timestamp (seconds) | Within ±5 minutes of server time |
X-Nonce |
Per-request unique value | 8–64 chars, never reused |
X-Callback-URL |
Per-transaction callback override | Optional — must point to the same scheme + host (+ port) as the webhook URL registered on your account |
Content-Type |
application/json |
— |
X-Callback-URLrestriction: the header is only accepted when it targets the same scheme + host (+ port) as your registered webhook URL — you can change the path, not the destination. A header pointing anywhere else (or sent while no webhook is registered on your account) is rejected with 400X-Callback-URL must match the firm's registered webhook host. If no callback URL can be resolved at all, the request fails with 400Callback URL not configured on provider. In production the callback URL must usehttpsand resolve to a public address, otherwise the request fails with 400Invalid callback URL: ….Platform integrations: if your payment platform (e.g. a turnkey/aggregator provider) has a central callback URL registered with HemenFast, all results are delivered there — this header and your account webhook are not used. See section 6.
Signature¶
Message to sign (lines joined with literal \n newlines):
HTTP_METHOD:POSTorGET(uppercase)PATH: e.g./paymentapi/deposit(path without query string)BODY: the raw JSON body (sign the exact bytes you send); forGETrequests with no body, an empty string — the message then contains an emptyBODYlineTIMESTAMP,NONCE: same values as the headers
Example (bash)¶
API_KEY="<api-key>"
SECRET_KEY="<secret-key>"
TIMESTAMP=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY='{"amount":"1000","bnk":"1","first_name":"Ali","last_name":"Yilmaz","username":"aliyilmaz","userid":"user001","transactionid":"TX-DEP-001","method":"fast"}'
MESSAGE="POST\n/paymentapi/deposit\n${BODY}\n${TIMESTAMP}\n${NONCE}"
SIGNATURE=$(printf "%b" "${MESSAGE}" | openssl dgst -sha256 -hmac "${SECRET_KEY}" | awk '{print $2}')
curl -X POST "https://api.hemenfast.com/paymentapi/deposit" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${API_KEY}" \
-H "X-Signature: ${SIGNATURE}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Callback-URL: https://yoursite.com/hemenfast/callback" \
-d "${BODY}"
Common mistake: the
BODYyou sign and the body you send must be byte-identical. Do not re-serialize the JSON — sign and send the same string.
Complete client implementations (PHP, Node.js, Python) are in section 10.
5. Endpoints¶
All request fields are JSON strings — including amount. Sending a number (e.g. "amount": 1000) or malformed JSON fails with 400 Invalid request format.
5.1 POST /paymentapi/deposit¶
Creates a deposit; routed to a payment account automatically. POST /paymentapi/getbank is an alias with identical behaviour.
Request:
{
"amount": "1000",
"bnk": "1",
"tcid": "12345678901",
"first_name": "Ali",
"last_name": "Yilmaz",
"username": "aliyilmaz",
"userid": "user001",
"transactionid": "TX-DEP-001",
"method": "fast"
}
| Field | Type | Required | Constraint |
|---|---|---|---|
amount |
string | Yes | Number > 0, . as decimal separator (e.g. "1000", "1000.50"); stored rounded to 2 decimals |
bnk |
string | Yes | 1–20 chars (payment channel / bank id) |
tcid |
string | No | National ID — no format check |
first_name |
string | Yes | 2–50 chars: letters (Turkish letters included), spaces, - ' . |
last_name |
string | Yes | 2–50 chars: letters (Turkish letters included), spaces, - ' . |
username |
string | Yes | 3–30 chars: letters, digits, _ . - |
userid |
string | Yes | 1–50 chars — your stable id for the end user |
transactionid |
string | Yes | 1–100 chars, unique (idempotency key) — see note below |
method |
string | No | fast, eft or havale; defaults to fast |
transactionidmust be unique across all your deposits and withdrawals, and it is never reusable — even after a rejection. Use a site-specific prefix (e.g.MYSITE-DEP-000123) and stick to URL-safe characters (A–Z a–z 0–9 - _ .), since the same value is used in thegetbyexternalidpath.
Success (200):
{
"status": true,
"msg": "Deposit received",
"bank_name": "Ziraat Bankasi",
"holder_name": "Mehmet Kaya",
"iban": "TR330006100519786457841326"
}
Show these account details to the user; payment goes to this account.
Business rejections — the request was valid but the deposit was not created. The body is always { "status": false, "msg": "<reason>" }:
msg |
Cause | HTTP | HTTP in legacy mode |
|---|---|---|---|
Bu islem numarasi zaten kullanilmis |
transactionid already used |
409 | 200 |
Halihazirda bekleyen talebiniz var |
This userid already has a pending deposit or withdrawal on your account |
409 | 200 |
Bu tutar icin uygun hesap yok |
No payment account available for this amount right now | 422 | 200 |
Yatırım şu anda kapalı |
Deposits are currently closed for your account (manually or by a scheduled pause window) | 403 | 200 |
Yatırım tutarı limit dışında |
Amount is outside the min/max deposit limits configured for your account | 403 | 200 |
Kullanıcı yasaklı |
The end user is on the block list (matched by userid or tcid) |
403 | 200 |
HTTP status codes for business rejections are on by default. Integrations that need every business rejection as HTTP 200 can be switched to legacy mode at onboarding; the body is the same in both modes. Either way, always branch on the
statusfield.The "one pending request per user" rule can be relaxed for platform integrations on request.
5.2 POST /paymentapi/withdraw¶
Creates a withdrawal.
Request:
{
"bankid": "1",
"amount": "5000",
"first_name": "Ali",
"last_name": "Yilmaz",
"tcid": "12345678901",
"iban": "TR330006100519786457841326",
"transactionid": "TX-WDR-001",
"userid": "user001",
"username": "aliyilmaz"
}
| Field | Type | Required | Constraint |
|---|---|---|---|
bankid |
string | Yes | 1–20 chars |
amount |
string | Yes | Number > 0 (same rules as deposit) |
first_name |
string | Yes | 2–50 chars: letters (Turkish letters included), spaces, - ' . |
last_name |
string | Yes | 2–50 chars: letters (Turkish letters included), spaces, - ' . |
tcid |
string | No | Optional |
iban |
string | No* | Up to 64 chars — send the user's TR IBAN (26 chars, e.g. TR33 0006 1005 …; spaces are allowed) |
transactionid |
string | Yes | 1–100 chars, unique (shared namespace with deposits, see 5.1) |
userid |
string | Yes | 1–50 chars |
username |
string | Yes | 3–30 chars: letters, digits, _ . - |
* A missing or malformed IBAN does not reject the request: it is stored as sent (normalized when it is a valid TR IBAN) and flagged as invalid to the operator. Always send the correct IBAN — the payout is made to it.
Success (200):
Business rejections ({ "status": false, "msg": "<reason>" }):
msg |
Cause | HTTP | HTTP in legacy mode |
|---|---|---|---|
Bu islem numarasi zaten kullanilmis |
transactionid already used |
409 | 200 |
Halihazirda bekleyen talebiniz var |
This userid already has a pending deposit or withdrawal on your account |
409 | 200 |
Çekim şu anda kapalı |
Withdrawals are currently closed for your account (manually or by a scheduled pause window) | 403 | 200 |
Kullanıcı yasaklı |
The end user is on the block list (matched by userid or tcid) |
403 | 200 |
5.3 GET /paymentapi/getbyid/{id}¶
Query status by HemenFast's internal transaction ID. You only see your own transactions.
Response (200):
{
"status": true,
"msg": "pending",
"data": {
"id": 1,
"transactionid": "TX-DEP-001",
"type": "deposit",
"status": "pending",
"amount": 1000.00,
"method": "fast",
"first_name": "Ali",
"last_name": "Yilmaz",
"username": "aliyilmaz",
"userid": "user001",
"tcid": "1234***01",
"assigned_bank_name": "Ziraat Bankasi",
"assigned_holder_name": "Mehmet Kaya",
"assigned_iban": "TR330006100519786457841326",
"created_at": "2026-03-30T12:00:00Z",
"updated_at": "2026-03-30T12:00:00Z"
}
}
msgrepeats the current status (pending,approvedorrejected).datais a slim, account-scoped view. Internal fields are not returned (firm_id,merchant_id,callback_url,processed_by,handling_by,amount_edited_by,original_amount,decision_note*,refund_*).tcidis masked (1234***01).amountis a JSON number with 2 decimals. Withdrawals additionally includebankidandiban; theassigned_*fields are present when a payment account is assigned.
Errors — note that these two status endpoints use an error field, not the status/msg envelope:
| HTTP | Body | Cause |
|---|---|---|
| 400 | { "error": "invalid id" } |
{id} is not an integer (getbyid only) |
| 404 | { "error": "transaction not found" } |
No such transaction, or it belongs to another account |
5.4 GET /paymentapi/getbyexternalid/{transactionid}¶
Query by the transactionid you submitted. Same response and error format as getbyid.
These endpoints are for status checks / reconciliation — they do not replace the callback, which remains the primary result notification. The status they return is always the authoritative current status.
6. Callbacks¶
When a transaction is approved or rejected, HemenFast sends an asynchronous POST to the callback URL. The destination is resolved in this priority order:
- Platform central callback URL — if your payment platform (turnkey/aggregator provider) has one registered with HemenFast, all results go there, signed with the platform's secret and signature scheme.
X-Callback-URLheader — per-transaction override (same-host restriction, see section 4).- Account webhook URL — the default registered on your account.
In cases 2 and 3 the callback is signed with your account's secret key as described below. In case 1 the platform verifies and relays the result to you through its own channels — the rest of this section then applies to the platform, not to your site.
The result is final at the moment it is decided; the callback notifies you of it. Delivery problems never change a transaction's status.
A reject callback is sent when a transaction is:
- rejected by an operator,
- automatically cancelled after staying
pendinglonger than the timeout configured for your integration (if any), or - a deposit that the operator marks for return/refund.
6.1 Payload¶
Sent as a JSON body (Content-Type: application/json) — not as form fields:
| Field | Description |
|---|---|
transid |
The transactionid you submitted |
amount |
Amount as a string, always 2 decimals (e.g. 1000.00) |
status |
approve or reject |
hashcode |
Signature (lowercase hex) — verify as below |
All values are JSON strings. The amount is the transaction's final amount, which can differ from the requested amount if an operator corrected it before deciding.
The
hashcodeis computed over the exactamountstring sent (always 2 decimals). Build the verification message from the values received verbatim — do not reformat them.
6.2 Verifying the hashcode¶
Callbacks to your account webhook or X-Callback-URL are always signed like this:
Platform callbacks (case 1) can use a different scheme configured for the platform — e.g. plain
sha256oversecret,transid,amount,status— and may carry extra payload fields. The active scheme is confirmed at onboarding.
PHP:
$payload = json_decode(file_get_contents('php://input'), true); // JSON body, not $_POST
if (!is_array($payload) || !isset($payload['transid'], $payload['amount'], $payload['status'], $payload['hashcode'])) {
http_response_code(400);
exit('invalid payload');
}
$message = $payload['transid'] . ',' . $payload['amount'] . ',' . $payload['status'];
$expected = hash_hmac('sha256', $message, $secretKey);
if (!hash_equals($expected, $payload['hashcode'])) {
http_response_code(400);
exit('invalid signature');
}
// safe: apply the result once (deduplicate on transid), then:
header('Content-Type: application/json');
echo json_encode(['status' => true, 'msg' => 'OK']);
Node.js (Express):
const crypto = require('crypto')
app.post('/hemenfast/callback', express.json(), (req, res) => {
const { transid, amount, status, hashcode } = req.body || {}
const message = `${transid},${amount},${status}`
const expected = Buffer.from(crypto.createHmac('sha256', SECRET_KEY).update(message).digest('hex'))
const received = Buffer.from(String(hashcode || ''))
// timingSafeEqual throws on different lengths, so compare lengths first
if (received.length !== expected.length || !crypto.timingSafeEqual(expected, received)) {
return res.status(400).send('invalid signature')
}
// apply the result once (deduplicate on transid), then:
res.status(200).json({ status: true, msg: 'OK' })
})
Python (Flask):
import hashlib
import hmac
from flask import request
@app.post("/hemenfast/callback")
def hemenfast_callback():
p = request.get_json(silent=True) or {}
message = f"{p.get('transid')},{p.get('amount')},{p.get('status')}".encode()
expected = hmac.new(SECRET_KEY.encode(), message, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, str(p.get("hashcode", ""))):
return "invalid signature", 400
# apply the result once (deduplicate on transid), then:
return {"status": True, "msg": "OK"}, 200
6.3 Your response¶
- Return any 2xx status within the timeout (15 seconds). Any other status — including 3xx, since redirects are not followed — is treated as a failure and retried.
- If the response body is a JSON object, it must contain
"status": true. A JSON object with"status": false, or without astatusfield (e.g.{"msg": "OK"}or{}), is treated as a failure and retried. An empty body or plain text (e.g.OK) is accepted. - Recommended response: HTTP 200 with
{"status": true, "msg": "OK"}. - Keep the handler fast: verify, record, respond. A response that arrives after the timeout counts as a failure even if you processed it, and the callback will be sent again.
6.4 Retry policy¶
| Property | Value |
|---|---|
| Max attempts | 3 in total (first delivery + 2 retries) |
| Retry spacing | Retries are scheduled 1 s, then 2 s after a failure; the delivery worker runs every 5 seconds, so a retry normally arrives within ~5–10 seconds |
| HTTP timeout | 15 seconds |
| Durability | Stored in an outbox before sending; pending deliveries are re-processed after a restart |
| Delivery guarantee | At-least-once — the same callback can arrive more than once |
| After the last failed attempt | Delivery stops; the transaction keeps its final status. Reconcile with the status endpoints (sections 5.3 / 5.4). |
6.5 Status mapping¶
| Transition | Callback status |
|---|---|
| pending → approved | approve |
| pending → rejected | reject |
A callback is sent only once per transaction, for its first and only result (
pending → approved/rejected). Later back-office status corrections on the panel (e.g.approved → rejected) do not emit a callback — treat the first result you receive as final and irreversible.Idempotency: the callback for a given
transidmay be re-delivered (retries, at-least-once delivery) — always with the samestatus. Deduplicate ontransid; never apply the same result twice.
7. Transaction Statuses¶
| Status | Description |
|---|---|
pending |
Created, awaiting result |
approved |
Approved (callback approve) |
rejected |
Rejected/cancelled (callback reject) |
8. Rate Limiting¶
- 100 requests/minute per API key (token bucket: bursts of up to 100 requests, refilling at ~1.7 requests/second).
- Requests that fail authentication are rejected before the limiter and do not count.
- On limit, HTTP 429:
9. Error Codes¶
| HTTP | Meaning |
|---|---|
| 200 | Success — in legacy mode also business rejections, with status: false (section 5) |
| 400 | Bad request / validation error / invalid callback URL |
| 401 | Authentication failed (HMAC) |
| 403 | Deposits/withdrawals closed, amount outside limits, blocked user — or account not set up for payments |
| 404 | Transaction not found (status endpoints) |
| 409 | Duplicate transactionid / pending request exists |
| 413 | Request too large (>1 MB) |
| 422 | No payment account available for this amount |
| 429 | Rate limit exceeded |
| 500 | Server error |
The deposit and withdraw endpoints (and authentication/rate-limit failures) share the response envelope { "status": false, "msg": "<reason>" } — the same shape as a success, with status: false. The two status endpoints use { "error": "<reason>" } instead (see 5.3). Always branch on the body, not the HTTP code alone.
9.1 Authentication failures (401)¶
The HMAC check runs before the request reaches any endpoint, in the order below. Each failure returns HTTP 401 with { "status": false, "msg": … }:
msg |
Cause & fix |
|---|---|
Authentication required: missing headers |
One of X-API-Key / X-Signature / X-Timestamp / X-Nonce is absent |
Invalid timestamp: invalid timestamp format / …: timestamp too old / …: timestamp too far in future |
X-Timestamp is not an integer, or is outside the ±5 minute window — send the current Unix time in seconds and keep your clock NTP-synced |
Invalid nonce: nonce must be between 8 and 64 characters |
X-Nonce length is out of range |
Invalid API key |
X-API-Key doesn't match an active account |
Invalid signature |
HMAC mismatch — recompute the signature per section 4, use lowercase hex, and make sure the body you sent is byte-identical to the body you signed |
Nonce replay detected |
This X-Nonce was already used with your API key — generate a fresh random nonce for every request |
9.2 Request errors (4xx / 5xx)¶
Returned by the deposit/withdraw endpoints after authentication passes, same { "status": false, "msg": … } envelope:
| HTTP | msg (examples) |
|---|---|
| 400 | Invalid request format (malformed JSON, or a field sent as a non-string) |
| 400 | Validation failed: <reason> — only the first problem is reported, e.g. Amount must be a positive number, FirstName must contain only letters, spaces, or - ' ., Username is too short, TransactionID is required |
| 400 | X-Callback-URL must match the firm's registered webhook host, Callback URL not configured on provider, Invalid callback URL: … (scheme / https / public-address check) |
| 403 | Firm not identified, Firm is not linked to an active provider — your account is not set up for payments; contact support |
| 413 | Request too large (body larger than 1 MB) |
| 429 | Rate limit exceeded. Please try again later. |
| 500 | An internal error occurred. Please try again later. |
Business rejections (duplicate transactionid, pending request, no account, closed, limits, blocked user) are listed in sections 5.1 and 5.2 — they return 409 / 422 / 403 (or HTTP 200 in legacy mode) with status: false.
Timeouts and 5xx: if a create request times out or returns 5xx, do not assume it failed. Query
getbyexternalidwith the sametransactionidfirst — or resend the identical request: if the first one was recorded you will getBu islem numarasi zaten kullanilmis(409), and the status endpoint returns the assigned account details. Never retry with a newtransactionidfor the same user action.
10. Sample Client Code¶
Each example signs and sends a request, covering both POST (JSON body) and GET (empty body — the BODY line in the signed message is an empty string). The same helper works for every /paymentapi/* endpoint; only the path and payload change. Callback verification examples are in section 6.2.
10.1 PHP¶
<?php
const BASE_URL = 'https://api.hemenfast.com';
const API_KEY = '<api-key>';
const SECRET_KEY = '<secret-key>';
function hemenfastRequest(string $method, string $path, ?array $payload = null): array
{
$body = $payload === null ? '' : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
// METHOD \n PATH \n BODY \n TIMESTAMP \n NONCE
$message = implode("\n", [$method, $path, $body, $timestamp, $nonce]);
$signature = hash_hmac('sha256', $message, SECRET_KEY);
$ch = curl_init(BASE_URL . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: ' . API_KEY,
'X-Signature: ' . $signature,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
],
]);
if ($body !== '') {
curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // send the exact string you signed
}
$response = curl_exec($ch);
curl_close($ch);
return json_decode((string) $response, true) ?? [];
}
// Deposit
$deposit = hemenfastRequest('POST', '/paymentapi/deposit', [
'amount' => '1000',
'bnk' => '1',
'first_name' => 'Ali',
'last_name' => 'Yilmaz',
'username' => 'aliyilmaz',
'userid' => 'user001',
'transactionid' => 'TX-DEP-001',
'method' => 'fast',
]);
if (!empty($deposit['status'])) {
// show $deposit['bank_name'], $deposit['holder_name'], $deposit['iban'] to the user
} else {
// e.g. "Bu tutar icin uygun hesap yok" or "Bu islem numarasi zaten kullanilmis"
}
// Status check (reconciliation)
$status = hemenfastRequest('GET', '/paymentapi/getbyexternalid/TX-DEP-001');
10.2 Node.js (18+)¶
const crypto = require('crypto')
const BASE_URL = 'https://api.hemenfast.com'
const API_KEY = '<api-key>'
const SECRET_KEY = '<secret-key>'
async function hemenfastRequest(method, path, payload) {
const body = payload ? JSON.stringify(payload) : ''
const timestamp = Math.floor(Date.now() / 1000).toString()
const nonce = crypto.randomBytes(16).toString('hex')
// METHOD \n PATH \n BODY \n TIMESTAMP \n NONCE
const message = [method, path, body, timestamp, nonce].join('\n')
const signature = crypto.createHmac('sha256', SECRET_KEY).update(message).digest('hex')
const res = await fetch(BASE_URL + path, {
method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
'X-Signature': signature,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
},
body: body || undefined, // send the exact string you signed
signal: AbortSignal.timeout(30_000),
})
return res.json()
}
// Deposit
const deposit = await hemenfastRequest('POST', '/paymentapi/deposit', {
amount: '1000',
bnk: '1',
first_name: 'Ali',
last_name: 'Yilmaz',
username: 'aliyilmaz',
userid: 'user001',
transactionid: 'TX-DEP-001',
method: 'fast',
})
// Status check (reconciliation)
const status = await hemenfastRequest('GET', '/paymentapi/getbyexternalid/TX-DEP-001')
10.3 Python¶
import hashlib
import hmac
import json
import secrets
import time
import requests
BASE_URL = "https://api.hemenfast.com"
API_KEY = "<api-key>"
SECRET_KEY = "<secret-key>"
def hemenfast_request(method: str, path: str, payload: dict | None = None) -> dict:
body = "" if payload is None else json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
# METHOD \n PATH \n BODY \n TIMESTAMP \n NONCE
message = "\n".join([method, path, body, timestamp, nonce])
signature = hmac.new(SECRET_KEY.encode(), message.encode(), hashlib.sha256).hexdigest()
resp = requests.request(
method,
BASE_URL + path,
headers={
"Content-Type": "application/json",
"X-API-Key": API_KEY,
"X-Signature": signature,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
},
data=body.encode() or None, # send the exact bytes you signed
timeout=30,
)
return resp.json()
# Deposit
deposit = hemenfast_request("POST", "/paymentapi/deposit", {
"amount": "1000",
"bnk": "1",
"first_name": "Ali",
"last_name": "Yilmaz",
"username": "aliyilmaz",
"userid": "user001",
"transactionid": "TX-DEP-001",
"method": "fast",
})
# Status check (reconciliation)
status = hemenfast_request("GET", "/paymentapi/getbyexternalid/TX-DEP-001")
Notes¶
- Withdraw uses the same helper:
POST /paymentapi/withdrawwith the fields from section 5.2. - Sign exactly what you send. All three helpers serialize the payload once and use the same string for both the signature and the request body. Non-ASCII names (e.g.
Şahin) are fine as long as the signed and sent bytes are the same UTF-8 string. X-Noncemust be unique per request — a reused nonce is rejected with 401Nonce replay detected. The examples generate a random 32-hex-char nonce each call.- Clock skew:
X-Timestampmust be within ±5 minutes of server time; keep your server clock NTP-synced. - To send a per-transaction
X-Callback-URLheader, remember the same-host restriction in section 4.