APW Wallet API

Partner API, version 1: registration, login, password reset, profile, limits and notifications, utility and other service payments, wallet-to-wallet transfers, currency exchange, ATM cash withdrawal, verification and operation history.

Overview

Base URL:

https://wallet.apw.ge/api/v1

Every request is a POST with a JSON body (Content-Type: application/json), including requests with an empty body.

Authentication happens at two levels:

  1. Partner: your server signs every request with an HMAC signature. This is how we know the request comes from you.
  2. Wallet customer: after login you send the received access_token in the Authorization: Bearer … header. This works exactly as in the APW Wallet app.

The secret must be stored only on the server. Signing always happens on your backend. Never put the secret in a mobile app or in browser code.

Your app calls your server, and your server calls this API.

Partner authentication (HMAC)

You will receive two values from us: a Partner ID (e.g. pk_…) and a secret.

The secret is handed over only once. If it is lost or leaked, contact us and we will issue a new one. The old one stops working at the same moment.

Headers

HeaderValue
X-Partner-IdYour Partner ID
X-TimestampCurrent time in unix seconds. Must not differ from the server time by more than ±300 seconds.
X-NonceA random string, unique for every request: 16–64 characters from A-Z a-z 0-9 _ -. For example, a UUID.
X-Client-IpThe IP address of the wallet customer (end user). Not the IP of your server.
X-SignatureHMAC-SHA256 in lowercase hex (see below)
Content-Typeapplication/json
AuthorizationBearer <access_token>. Required only on endpoints marked “customer token”.

String to sign

Six lines joined with \n (LF). No trailing newline is added at the end.

POST
/api/v1/auth/login-check
1761140000
3f1c9a0e-5b7d-4e2a-9c61-0d8e2b7f4a10
203.0.113.7
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  1. The HTTP method in uppercase: POST.
  2. The URL path, without the domain and query string. For example /api/v1/auth/login-check.
  3. The value of X-Timestamp.
  4. The value of X-Nonce.
  5. The value of X-Client-Ip.
  6. The SHA-256 hash of the request body, in lowercase hex. The body must be exactly the bytes you send. The hash of an empty body is e3b0c442…b855.
X-Signature = hex( HMAC-SHA256( key = secret, message = string to sign ) )

Serialize the JSON first, then sign that string, and send exactly the same string.

If your HTTP library re-serializes the object when sending it (changing whitespace, field order or number formatting), the signature will not match and you will get error 906.

function apwRequest(string $path, array $data, string $clientIp, ?string $customerToken = null): array
{
    $partnerId = getenv('APW_PARTNER_ID');
    $secret    = getenv('APW_SECRET');

    $body      = json_encode($data ?: new stdClass(), JSON_UNESCAPED_UNICODE);
    $timestamp = (string) time();
    $nonce     = bin2hex(random_bytes(16));

    $stringToSign = implode("\n", [
        'POST', $path, $timestamp, $nonce, $clientIp, hash('sha256', $body),
    ]);

    $headers = [
        'Content-Type: application/json',
        'X-Partner-Id: ' . $partnerId,
        'X-Timestamp: ' . $timestamp,
        'X-Nonce: ' . $nonce,
        'X-Client-Ip: ' . $clientIp,
        'X-Signature: ' . hash_hmac('sha256', $stringToSign, $secret),
    ];
    if ($customerToken) {
        $headers[] = 'Authorization: Bearer ' . $customerToken;
    }

    $ch = curl_init('https://wallet.apw.ge' . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 140,
    ]);
    $response = curl_exec($ch);
    $status   = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);

    return ['status' => $status, 'body' => json_decode((string) $response, true)];
}

$result = apwRequest('/api/v1/auth/login-check', [
    'wallet_number' => '12345678901',
    'password'      => 'Secret123',
], $_SERVER['REMOTE_ADDR']);

Additional protection

Customer authentication

The access_token comes from three endpoints: auth/login, register/4 (completing registration) and password/reset/change. Send it in the Authorization: Bearer <token> header, together with the partner HMAC headers.

Responses and errors

Gateway errors (9xx)

The request did not reach the wallet. The format is {"errorCode": 9xx, "errorMessage": "..."}.

errorCodeHTTPReason
901400A header is missing or has an invalid format
902401Partner ID is unknown or disabled
903403The request came from a disallowed IP address
904401X-Timestamp is outside the ±300 second window
905401X-Nonce has already been used
906401The signature does not match
907429The per-minute request limit has been exhausted
908404No such endpoint exists
910502The wallet service is temporarily unavailable
911504The wallet service did not respond in time. The request may still have been executed, so check the result before retrying.

Wallet responses

After the signature check, the wallet response is returned unchanged: both the status and the body. The body comes in two formats:

GroupSuccessError
auth/*, password/reset/*, two-factor/*HTTP 200, errorCode: 0HTTP 200, errorCode ≠ 0 and errorMessage
register/*, user/passwordHTTP 200HTTP 402, {"message": "..."}
billing/pay, billing/info, transfer/*HTTP 200HTTP 402, errorCode + errorMessage or {"message": "..."}
user, user/transactions, user/limits, billing catalogHTTP 200, data directly (without a wrapper)401 / 402
user/services, user/notifications/*, exchange/*HTTP 200, errorCode: 0 and dataHTTP 402, errorCode + errorMessage (message on validation errors)
verification/*, verification-defect/*HTTP 200HTTP 402, {"message": "..."}
requested-documentHTTP 200, errorCode: 0HTTP 200, errorCode ≠ 0
qr/verifyHTTP 200, errorCode: 0HTTP 401 / 402, errorCode + errorMessage or {"message": "..."}

Read the error text in this order: errorMessage, then message. The text (errorMessage / message) is intended to be shown to the customer. It is mostly in Georgian, some of it in English. Build your logic on errorCode and the HTTP status, not on the text.

Limits

Registration: flow

Registration consists of several steps. Each step uses the token from the previous one (a 64-character string; this is not the access_token).

Two steps take place on the customer's own device and do not go through the API:

  1. register/params: lists for the form (countries, employment, income…).
  2. register/1: contact details, address, password, PEP status. Returns a token.
  3. register/2: economic profile. Returns an iframe_url.
  4. The customer opens the iframe_url on their device (a camera is required) and completes the document scan and selfie.
  5. register/3: we check the document, and the customer receives two SMS messages: the contract link and a 6-digit code.
  6. The customer signs the contract via the link. The link is valid for 10 minutes.
  7. register/check: poll until status: 1 is returned. If the link has expired, use register/contract-sms.
  8. register/4: registration is completed with the SMS code. Returns an access_token.

Restrictions:

  • Registration is currently available only to citizens of Georgia.
  • The customer must be at least 18 years old.
  • An ID card or a passport is accepted as the document.

POST/api/v1/register/params

Allowed values for the form fields. The body is empty: {}.

{
  "message": "Success",
  "data": {
    "0": {
      "countrie": [{"ccode": "GE", "country": "Georgia", "code": "995", ...}],
      "sferos": [...],
      "userEmployeeStatus": [...],
      "userExpectedTurnover": [...],
      "userMonthlyIncome": [...],
      "userPurposeRelationship": [...],
      "userSourceOfIncome": [...],
      "currency_id": [{"id": 981, "name": "GEL"}, {"id": 978, "name": "EUR"}, {"id": 840, "name": "USD"}, {"id": 643, "name": "RUB"}]
    },
    "links": {"contract": "https://…pdf", "privacy_policy": "https://…pdf", "limits": "https://…pdf"},
    "pep_status": [...], "pep_family_status": [...],
    "pep_definition": "...", "pep_family_definition": "..."
  }
}

The lists are under data["0"] (the key is the string "0").

POST/api/v1/register/1

FieldRule
mobileRequired. 9–15 digits, including the country code (995555123456). Must be unique.
emailRequired. A valid and unique email address.
legal_addressRequired. 3–255 characters.
address_sameRequired. 1: the actual address matches the legal address; 0: it does not.
real_addressRequired if address_same = 0. 3–255 characters.
passwordRequired. At least 8 characters, containing both letters and digits.
confirm_passwordRequired. Must match password.
pep_statusRequired. 0 or 1: whether the person is a politically exposed person.
pep_titleRequired if pep_status = 1. 3–255 characters.
pep_family_statusRequired. 0 or 1.
pep_familyRequired if pep_family_status = 1. 3–255 characters.
limits, privacy_policy, contractRequired. All three must be 1: the customer has agreed to the documents given in links.
// 200
{"token": "Xr9…64 characters", "message": "Success"}
// 402
{"message": "ელ-ფოსტა უკვე გამოყენებულია"}

POST/api/v1/register/2

FieldRule
tokenThe token from register/1
dual_citizen1: has dual citizenship; 2: does not
country2Required if dual_citizen = 1. 2-letter country code (ccode).
employee_statusID from userEmployeeStatus
job_title, occupied_position3–100 characters. Both are required when employee_status = 1. When = 2, only occupied_position.
source_of_income_idRequired if employee_status = 3. ID from userSourceOfIncome.
company_name, company_countryRequired if employee_status is 4 or 5. company_country is a 2-letter code.
monthly_incomeID from userMonthlyIncome
expected_turnoverID from userExpectedTurnover
purpose_idID from userPurposeRelationship
currenciesComma-separated currencies from 981,840,978,643, e.g. "981,840"
// 200
{"token": "…", "iframe_url": "https://widget.identomat.com/?session_token=…", "message": "Success"}

Open the iframe_url for the customer: in a WebView with camera permission, or in a browser. When the customer finishes the scan, call register/3.

POST/api/v1/register/3

Field: token.

We check the Identomat result, the validity of the document and the age. On success the customer receives two SMS messages: the contract link and a 6-digit code for register/4. This step takes a few seconds.

// 200
{"token": "…", "mobile": "995******123", "expire": 5, "message": "Success"}
// 402: examples
{"message": "user_checking"}                    // the document is being checked manually, try again later
{"message": "user must be 18 years or older!"}
{"message": "არარეზიდენტების რეგისტრაცია დროებით შეზღუდულია!"}

POST/api/v1/register/check

Field: token. Checks whether the customer has signed the contract. Poll once every few seconds.

// 200: signed
{"status": 1, "message": "ხელმოწერილი"}
// 402: not signed yet. This is a normal state, not an error.
{"status": 0, "message": "ხელმოუწერელი"}

POST/api/v1/register/contract-sms

Field: token. Resends the contract link. Call it only when the previous link has expired, i.e. 10 minutes after it was sent.

// 200
{"token": "…", "mobile": "99*****23", "expire": 5, "message": "Success"}

POST/api/v1/register/4

FieldRule
tokenThe token from register/3
sms6 digits: the code from the register/3 SMS
// 200
{
  "message": "რეგისტრაცია წარმატებით დასრულდა",
  "user": {"id": 1, "verify_id": 1, "iban": "GE…", "wallet_number": "12345678901",
           "first_name": "…", "last_name": "…", "birth_date": "1990-01-01",
           "balance": 0, "balance_usd": 0, "balance_eur": 0, "balance_rub": 0,
           "mobile": "995…", "email": "…"},
  "access_token": "eyJ0eXAiOiJKV1Qi…"
}
// 402
{"message": "ხელშეკრულებაზე დათანხმება აუცილებელია!"}

Login: flow

  1. auth/login-check: wallet number and password. The two_factor field in the response tells you which code is required.
  2. two_factor: "sms": the customer receives an SMS code. To resend it, use auth/login-sms.
    two_factor: "google": the code is in the customer's Authenticator app. No SMS is sent.
  3. auth/login: token and code. Returns an access_token.

Legal entity (type = 2): one natural person can have several companies linked to them. auth/login-check returns a legal_entities list; if it contains more than one entry, you must pass legal_user_id to auth/login. With a single company nothing changes.

POST/api/v1/auth/login-check

FieldRule
wallet_numberRequired. 11 digits.
passwordRequired
typeOptional. 1: natural person (default); 2: log in on behalf of a legal entity that the customer manages.
// SMS
{"two_factor": "sms", "phone": "995******123", "token": "…", "date": "2026-10-22 12:00:00",
 "expire": 1, "errorCode": 0, "message": "Sms send success"}
// Authenticator
{"two_factor": "google", "token": "…", "date": "…", "expire": 5, "errorCode": 0,
 "message": "Authenticator code required"}

expire is the code's validity period in minutes.

For type = 2, the response additionally contains legal_entities — the list of companies this customer can log in as. This field is absent from the response for a natural-person login.

{"two_factor": "sms", "phone": "995******123", "token": "…", "expire": 1,
 "errorCode": 0, "message": "Sms send success",
 "legal_entities": [
   {"legal_user_id": 12, "wallet_number": "00000000123",
    "name": "შპს ალფა", "identification_number": "400147211"},
   {"legal_user_id": 19, "wallet_number": "00000000456",
    "name": "შპს ბეტა",  "identification_number": "405123456"}
 ]}

The list includes only the companies that can be logged in as at this moment — expired or suspended authority and blocked companies are excluded. name is the company name, identification_number is its identification code; both are meant to be shown to the customer, while legal_user_id is what you pass to auth/login.

errorCodeMeaning
1Validation error
2, 4Wrong wallet number or password, or the number of attempts has been exhausted
3The authorization is undergoing an additional check or is blocked. Show the text to the customer and try again later.
5An SMS was sent just now. Try again in 1 minute.
6Legal entity not found (type = 2)
8Companies are linked, but none can be logged in as at this moment (type = 2): the power of attorney has expired, is not specified, has not started yet, or the authority is suspended/revoked. Logging in as a natural person is still possible.

POST/api/v1/auth/login-sms

Field: token (from login-check). Resends the SMS code, at most once per minute. The token does not change.

{"phone": "995******123", "token": "…", "date": "…", "expire": 1, "errorCode": 0, "errorMessage": "Sms send success"}

If errorCode: 6 is returned, the customer uses an Authenticator app and no SMS is sent.

POST/api/v1/auth/login

FieldRule
tokenThe token from login-check
sms6-digit SMS code. Valid for 1 minute.
code6-digit Authenticator code, when two_factor: "google". Must be used within 5 minutes of login-check.
legal_user_idOptional. Which legal entity to log in as — from login-check's legal_entities. Required only when the list contains more than one entry; omit it if there is only one.
{
  "user": {"id": 1, "verify_id": 1, "iban": "GE…", "wallet_number": "12345678901",
           "first_name": "…", "last_name": "…", "birth_date": "…", "mobile": "…", "email": "…",
           "balance": 10.5, "balance_usd": 0, "balance_eur": 0, "balance_rub": 0,
           "frozen_balance": 0, "frozen_balance_usd": 0, "frozen_balance_eur": 0, "frozen_balance_rub": 0,
           "type": 1, "verification_defect": 0, "requested_document": 0, ...},
  "access_token": "eyJ0eXAiOiJKV1Qi…",
  "errorCode": 0,
  "errorMessage": "success"
}
errorCodeMeaning
1Validation error
2The code is wrong or expired, or the attempts have been exhausted. In the last case, start the login over from login-check.
5Legal entity not found
7Authenticator code attempts exhausted
8The authority ended after login-check (see login-check, code 8)
9Choose a legal entity: you did not send legal_user_id and the list has more than one company, or the id you sent does not belong to this customer. The response includes legal_entities.

On code 9 the token remains valid — show the choice and call again with the same token and code. The login does not start over.

// 200 — choose a company
{"errorCode": 9, "errorMessage": "აირჩიეთ იურიდიული პირი",
 "legal_entities": [
   {"legal_user_id": 12, "wallet_number": "00000000123",
    "name": "შპს ალფა", "identification_number": "400147211"},
   {"legal_user_id": 19, "wallet_number": "00000000456",
    "name": "შპს ბეტა",  "identification_number": "405123456"}
 ]}

A successful login revokes all other sessions of this account. For a legal entity this also means that two authorized persons of the same company cannot be logged in at the same time — the second one's login logs out the first.

POST/api/v1/auth/logoutcustomer token

The body is empty. Revokes the customer's tokens.

{"errorCode": 0, "errorMessage": "success"}

Password reset: flow

  1. password/reset/check: wallet number. The customer receives an SMS code.
  2. password/reset/verify: the code. Returns a new token.
  3. password/reset/change: the new password, with the new token, within 10 minutes. The customer is logged in directly.

password/reset/check returns success even for an unknown wallet number. This is intentional, so that the API cannot be used to find out whether a wallet exists. In that case the next step will return a “wrong code” error.

POST/api/v1/password/reset/check

Field: wallet_number (11 digits).

{"phone": "995******123", "token": "…", "date": "…", "expire": 1, "errorCode": 0,
 "errorMessage": "Password reset sms send success"}

Errors: 1 validation · 3 authorization is being checked or is blocked · 4 an SMS was sent just now.

POST/api/v1/password/reset/sms

Field: token. Resends the SMS, at most once per minute. The response format is the same as for reset/check.

POST/api/v1/password/reset/verify

FieldRule
tokenThe token from reset/check
sms6 digits. Valid for 1 minute.
{"token": "…new token…", "date": "…", "errorCode": 0, "errorMessage": "SMS check success"}

Errors: 1 validation · 2 the code is wrong or attempts are exhausted · 3 the code has expired.

POST/api/v1/password/reset/change

FieldRule
tokenThe new token from reset/verify
passwordAt least 8 characters, containing both letters and digits
confirm_passwordMust match password

The success response is the same as for auth/login: user and access_token. Errors: 1 validation · 2 the token is wrong or already used · 3 the token has expired (10 minutes).

Payments: flow

Paying for services from the customer's wallet: mobile, utilities, internet, etc. The catalog is public (no customer token needed), while paying requires a token.

  1. billing/categories → billing/category/{id} → billing/service/{id}: choosing a service. The service contains the field descriptions (params_info, params_pay), the amount limits and the commission.
  2. billing/info: checking the subscriber (debt, name…). The sms field in the response tells you whether the payment needs an SMS code.
  3. If sms = 1: billing/pay-sms. The code is valid for 2 minutes.
  4. billing/pay: the payment. On success you receive an operationId.
  5. billing/status/{operationId}: poll until the status becomes 3 (completed) or 4 (cancelled).

Payment is asynchronous. Success of billing/pay means the payment has been accepted. At that moment the amount is held on the wallet, and it is debited on completion, usually within a few seconds. Check the final result with billing/status.

If you receive the response „ოპერაცია მუშავდება…“ ("operation in progress…") or 911 / a timeout, do not repeat the payment. The result is unknown and the payment may already have been executed. Check user/transactions: the entry will appear there as „მიმდინარე: …“ ("in progress: …"). Repeating it will cause a double payment.

The currency is always the lari (GEL). An identical payment (same service, subscriber and amount) is not accepted a second time within 20 seconds.

Catalog

POST/api/v1/billing/categories: array of categories.

[{"id": 32, "image": "https://uploads.allpayway.ge/files/categories/….png", "count": 3,
  "lang": {"ka": "მობილური", "en": "Mobile", "ru": "Мобильная связь"}}, ...]

POST/api/v1/billing/category/{id}: the services of a category.

{"services": [{"id": 19, "name": "Magti", "lang": {...}, "image": "…", "min_amount": "1.00", "max_amount": "400.00", ...}],
 "category": {"lang": {"ka": "…", "en": "…", "ru": "…"}}}

POST/api/v1/billing/services: all services in a single array.

POST/api/v1/billing/service/{id}: full description of a single service.

{
  "errorCode": 1000,
  "service": {
    "id": 19, "name": "Magti", "lang": {"ka": "მაგთი", "en": "Magti", "ru": "Магти"},
    "currency_id": 981, "min_amount": "1.00", "max_amount": "400.00", "category_id": 32,
    "image": "https://uploads.allpayway.ge/files/services/….png",
    "params_info": [
      {"name": "phone_number", "regexp": "/^(\\d{9})$/i", "description": "ტელეფონის ნომერი", "example": "599470076"}
    ],
    "params_pay": [
      {"name": "phone_number", "regexp": "/^(\\d{9})$/i", "description": "ტელეფონის ნომერი", "example": "599470076"}
    ],
    "commission": {
      "client_commission_percent": "4.00", "client_commission_fixed": "0.00",
      "min_client_commission": "0.000000", "max_client_commission": "0.000000",
      "min_amount": "1.000000", "max_amount": "1000.000000", "rate": 1
    }
  }
}

POST/api/v1/billing/param: description of a single field. Body: {"param": "phone_number"}.

POST/api/v1/billing/info

Checking the subscriber. Fields: service_id and every params_info field of the service.

// request
{"service_id": 19, "phone_number": "599470076"}
// 200: the fields depend on the service. "sms" is always present.
{"errorCode": 1000, "errorMessage": "წარმატებული", ..., "sms": 1}
// 402
{"message": "The phone number field is required."}
{"errorCode": 99, "errorMessage": "00:10 საათამდე გადახდას ვერ შეძლებთ"}

POST/api/v1/billing/pay-smscustomer token

The customer receives a 6-digit code to confirm the payment. The code is valid for 2 minutes. The body is empty.

{"message": "Success"}

A customer has one active SMS code at a time. It is shared between payment, transfer, login and verification-defect/2. Every new send revokes the previous code. Request the code immediately before pay.

POST/api/v1/billing/paycustomer token

FieldRule
service_idService ID
The service's params_pay fieldsEach with its own regexp
amountAmount in GEL, with at most 2 decimal places and greater than 0 ("10", "10.50"). Within the service's min_amount/max_amount.
sms6-digit code, if billing/info returned sms: 1
// request
{"service_id": 19, "phone_number": "599470076", "amount": "10.00", "sms": "123456"}
// 200: payment accepted
{"errorCode": 1000, "errorMessage": "წარმატებული", "operationId": 987654,
 "accoutant": {"genAmount": 10.4, "clientCommission": 0.4}, ...}

Errors (HTTP 402):

ResponseMeaning
errorCode 9Payment rejected: insufficient balance, limit exhausted, SMS code wrong or expired, customer not verified, or the same payment was sent just now. The reason is in errorMessage.
errorCode 9 + „ოპერაცია მუშავდება…“The result is unknown. Do not repeat. Check user/transactions.
errorCode 8The customer's ID document has expired. It needs to be renewed.
errorCode 99Nightly break, 23:30–00:10
Other errorCodeRejected by the provider. The amount was not debited.
{"message": "..."}Field validation or document check error
HTTP 401 „ავტორიზაცია მუშავდება“An additional check is in progress. Try again later. The SMS code remains valid within its 2 minutes.

POST/api/v1/billing/status/{operationId}customer token

operationId from the billing/pay response. The body is empty. The response is always HTTP 200.

{"errorCode": 0, "errorMessage": "success", "data": {
  "service": {"image": "…", "lang": {"ka": "მაგთი", "en": "Magti", "ru": "Магти"},
              "abonent": "599470076", "amount": "10.00", "gen_amount": "10.40", "client_comision": "0.40"},
  "status": {"status_id": 3, "description": "შესრულებული"}}}
status_idMeaningWhat to do
1RegisteredPoll again
2In progressPoll again
3CompletedFinal: the amount was debited
4CancelledFinal: the amount was released
5Pending confirmationProcessed manually, contact us

If the payment is not found, service and status are empty arrays ([]).

Poll once every 5–10 seconds.

Transfers: flow

From an APW wallet to an APW wallet, instantly. All endpoints require a customer token.

  1. transfer/info: checking the recipient (by wallet number or IBAN) and the commission. Optional.
  2. transfer/pay-sms: confirmation code via SMS. Valid for 2 minutes.
  3. transfer/pay: the transfer.

POST/api/v1/transfer/infocustomer token

FieldRule
type1: by wallet number; 2: by IBAN
wallet_number11 digits (when type = 1)
ibanIn the format GE00DL0000000000000000 (when type = 2)
// 200
{"errorCode": 0, "errorMessage": "Success",
 "data": {"reciver": "გ. ბ.", "iban": "GE00DL0000000000000000", "percents": ...}}

reciver: the recipient's initials, so the customer can double-check. percents: the transfer commission.

Errors (402): 1 validation · 2 service restricted · 3 recipient not found · 4 transfer to own wallet · 22 temporary technical issue.

POST/api/v1/transfer/pay-smscustomer token

Same as billing/pay-sms: a 6-digit code valid for 2 minutes, and it is the same shared code (see note). Response: {"message": "Success"}.

POST/api/v1/transfer/paycustomer token

FieldRule
type, wallet_number / ibanAs in transfer/info
amountAt most 2 decimal places ("25.50")
currency_id981 GEL · 840 USD · 978 EUR · 643 RUB
destinationPurpose of the transfer (text)
sms6-digit code from transfer/pay-sms
// request
{"type": 1, "wallet_number": "12345678901", "amount": "25.50", "currency_id": 981,
 "destination": "ვალის დაბრუნება", "sms": "123456"}
// 200: transfer completed
{"errorCode": 0, "errorMessage": "Success", "data": {...}}

Errors (HTTP 402, errorCode):

errorCodeMeaning
1Validation
2–9Sender check failed: restriction, document, limit (9)
10–18Recipient check failed
19Insufficient balance
20Wrong SMS code
21SMS code expired
22The result is unknown (the transfer service did not respond). Do not repeat; check user/transactions first.
HTTP 401 „ავტორიზაცია მუშავდება“Additional check. Try again later.

Customer

POST/api/v1/usercustomer token

The customer's current data and balances. The body is empty. The response is an object without a wrapper:

{"id": 1, "iban": "GE…", "wallet_number": "12345678901", "first_name": "…", "last_name": "…",
 "birth_date": "…", "mobile": "…", "email": "…",
 "balance": 10.5, "balance_usd": 0, "balance_eur": 0, "balance_rub": 0,
 "frozen_balance": 0, "frozen_balance_usd": 0, "frozen_balance_eur": 0, "frozen_balance_rub": 0,
 "avatar": null, "verify_id": 1, "type": 1,
 "verification_defect": 0, "requested_document": 0}

POST/api/v1/user/transactionscustomer token

Operation history, newest first. All filters are optional. Do not send a field you are not using at all (an empty string causes a validation error).

FieldRule
from_date, to_dateY-m-d, both days inclusive
from_amount, to_amountAmount range
currency_id981 / 840 / 978 / 643
limitNumber of entries, 10 by default. Please do not request more than 100.
[{"id": 1201, "date": "2026-10-22 12:00:05", "description": "Magti: 599470076",
  "amount": 10.4, "type": 0, "balance": 89.6, "currency_id": 981, "currency": "GEL",
  "service_id": 19, "abonent": "599470076", "gen_amount": "10.40", "client_comision": "0.40", "status_id": 3}, ...]

POST/api/v1/user/passwordcustomer token

FieldRule
old_passwordCurrent password
new_passwordAt least 8 characters, containing both letters and digits
confirm_passwordMust match new_password
// 200
{"message": "პაროლი წარმატებით შეიცვალა"}
// 402
{"message": "ძველი პაროლი არასწორია!"}

POST/api/v1/user/language/{locale}customer token

locale: ge, en or ru. Stores the customer's preferred language (used on payment pages). The body is empty.

{"message": "success"}

POST/api/v1/user/limitscustomer token

The customer's spending limits in GEL: monthly and daily. The body is empty. The response has no wrapper.

// natural person
{"month": "5000.00", "day": "1500.00"}
// legal entity
{"month": "unlimited", "day": "unlimited"}

Read both fields as strings: they can be a number or "unlimited".

POST/api/v1/user/servicescustomer token

Services payable with the wallet, grouped by category, with amount limits and the customer commission. The body is empty. For a full service description (fields, logo, names in each language) use billing/service/{id}.

{"errorCode": 0, "errorMessage": "success", "data": [
  {"category_id": 32, "services": [
    {"service_id": 19, "name": "Magti", "min_amount": 1.0, "max_amount": 400.0,
     "client_commission_percent": 4.0, "client_commission_fixed": 0.0,
     "min_client_commission": 0.0, "max_client_commission": 0.0}, ...]}, ...]}

If there are no entries: HTTP 402, {"errorCode": 1, "errorMessage": "ჩანაწერები ვერ მოიძებნა"}.

POST/api/v1/user/kyccustomer token

The customer's KYC documents (the questionnaire and its confirmation). The body is empty.

{"errorCode": 0, "errorMessage": "success", "data": [
  {"id": 123, "file": "https://…/files/kyc/…?expires=…&sig=…", "date": "2026-10-22T10:00:00.000000Z"}, ...]}

file is a signed link, valid for 1 hour. Do not store it: call the endpoint again when you need it. These are personal documents; show them to no one but the customer. If there are no documents, data is an empty array.

Notificationscustomer token

APW notifications for the customer. The body is empty for all endpoints. Notification format:

{"id": 5, "user_id": 42, "type_id": 1, "seen": 0, "seen_at": null,
 "title": "…", "text": "…", "created_at": "…", "updated_at": "…"}
EndpointReturns
/api/v1/user/notifications/nextOne unread notification in data. If there is none, data is null. Order is not guaranteed.
/api/v1/user/notifications/newAll unread ones, as an array
/api/v1/user/notifications/all/{limit}The last limit notifications, newest first, including read ones. limit: 1–100.
/api/v1/user/notifications/seen/{id}Marks the notification as read
{"errorCode": 0, "errorMessage": "success", "data": [ … ]}

seen/{id} always returns {"errorCode": 0, "errorMessage": "success"}, even when the notification does not exist or belongs to another customer (someone else's notification is not changed). next does not mark the notification as read.

Two-factor authentication (Authenticator)customer token

By default the login code is sent via SMS. The customer can link Google Authenticator or another TOTP app.

All endpoints return HTTP 200. On success: {"errorCode": 0, "errorMessage": "success", "data": {...}}.

Current state (data):

{"method": "sms" | "google", "method_id": 1 | 2, "google_enrolled": true,
 "google_confirmed_at": "2026-10-22 12:00:00" | null, "google_setup_pending": false}
EndpointFieldsWhat it does
/api/v1/two-factor—Returns the current state
/api/v1/two-factor/google/setup—Starts linking. data: secret, otpauth_uri, digits, period. Show otpauth_uri as a QR code.
/api/v1/two-factor/google/confirmcode (6 digits, from the app)Confirms linking. After this, login requires the Authenticator code.
/api/v1/two-factor/google/cancel—Cancels an unfinished linking
/api/v1/two-factor/google/disablecodeDisables the app, and login goes back to SMS
/api/v1/two-factor/methodmethod: sms or google; code, if switching from google to smsChanges the login method

Errors: 1 validation · 2 the code is wrong or expired · 3 the app is not linked.

Currency

Exchange rates and currency exchange between wallet balances. All endpoints require a customer token.

POST/api/v1/exchange/currenciescustomer token

The body is empty.

{"errorCode": 0, "errorMessage": "Success", "data": [
  {"id": 981, "title": "GEL"}, {"id": 840, "title": "USD"},
  {"id": 978, "title": "EUR"}, {"id": 643, "title": "RUB"}]}

RUB (643) is in the list, but a rate cannot be obtained for it (errorCode 2).

POST/api/v1/exchange/ratecustomer token

How much the customer would receive when exchanging amount. No money is moved.

FieldRule
fromSource: 3-digit currency code (981, 840, 978)
toTarget: 3-digit currency code
amountAmount in the from currency, with at most 2 decimal places
// request
{"from": 981, "to": 840, "amount": 100}
// 200
{"errorCode": 0, "errorMessage": "Success",
 "data": {"from": "GEL", "to": "USD", "amount": 100, "rate": 36.42}}

data.rate is the amount to be received in the to currency, not the unit rate.

errorCode (HTTP 402)Meaning
9Validation. The text is in message.
1A parameter is missing
2The currency is invalid or is RUB, there is no rate for today
3Customer not found
4Customer is not verified
5Customer is blocked
6AML restriction
7Sanctions screening failed for an amount over 2500

POST/api/v1/exchange/convertcustomer token

Performs the exchange: the amount is debited from the from balance and credited to the to balance. Fields and the success response are the same as for exchange/rate. data.rate is the amount credited.

// request
{"from": 981, "to": 840, "amount": 100}
// 200: exchange completed
{"errorCode": 0, "errorMessage": "Success",
 "data": {"from": "GEL", "to": "USD", "amount": 100, "rate": 36.42}}

No SMS code is required: have the customer confirm the exchange in your application before you call it.

Do not retry on 911 / timeout. The outcome is unknown: check user/transactions first. While an exchange is still being processed, a new exchange for the same customer is answered with errorCode 11.

ResponseMeaning
402, errorCode 1–7As for exchange/rate
402, errorCode 8Amount below 1, insufficient balance, or the customer has no valid document
402, errorCode 9Validation (the text is in message) or a write failure
402, errorCode 11A previous exchange for this customer is still being processed. Try again later.
402, {"message": "..."}The document check failed, or the document is cancelled or suspended
401 „ავტორიზაცია მუშავდება“Additional check (see the exception). Try again later.

Verification: what to use when

The user response contains three fields that tell you what the customer needs to do:

SituationWhat to use
The ID document has expired (billing/pay returns errorCode 8) or a new document is requiredverification/1 → verification/2: re-scan of the document and a selfie
verification_defect: 1verification-defect/1 → 2 → 3: filling in the missing data
requested_document: 1requested-document: uploading the requested image. Which image is needed is written in requested_document_comment.

POST/api/v1/verification/{step}customer token

step: 1 or 2. The process resembles the document step of registration (register/2–3).

  1. verification/1: the body is empty. Returns a token and an iframe_url.
    {"token": "…64 characters", "iframe_url": "https://widget.identomat.com/?session_token=…", "message": "Success"}
  2. Open the iframe_url for the customer (a camera is required). The widget language is Georgian. If session_token in the URL is empty, the service is temporarily unavailable: try again later.
  3. verification/2: field token. Checks the document and saves it. This may take several minutes: set a timeout of at least 310 seconds.
    // 200
    {"message": "Verification completed Success"}
    // 402: examples
    {"message": "user_checking"}            // the document is being checked manually, try again later
    {"message": "არასწორი დოკუმენტი!"}      // the document belongs to another person
    {"message": "SESSION_NOT_FOUND"}        // the token is wrong or the scan was not completed

The personal number on the document must match the customer's personal number.

POST/api/v1/verification-defect/{step}customer token

step: 1, 2 or 3. Use it when user has verification_defect: 1: data is missing from the customer's profile form.

verification-defect/1: the body is empty. Returns which fields need to be filled in.

// needs filling in
{"errorCode": 1, "errorMessage": "შესავსებია გამოტოვებული ველები",
 "data": {"employee_status": …, "real_address": …}}
// nothing is missing
{"errorCode": 0, "errorMessage": "ვერიფიცირებული"}

The keys of data come from this list: employee_status, monthly_income, expected_turnover, address_same, job_title, occupied_position, source_of_income_id, company_name, company_country, real_address, legal_address. Both responses are HTTP 200.

verification-defect/2: send only the fields returned by step 1. The value lists are in register/params.

FieldRule
employee_status, monthly_income, expected_turnoverID from the corresponding list
job_title, occupied_position3–100 characters. Both are required when employee_status = 1; when = 2, only occupied_position.
source_of_income_idRequired if employee_status = 3
company_name, company_countryRequired if employee_status is 4 or 5. company_name 2–100 characters, company_country a 2-letter code.
legal_address, real_address3–255 characters

address_same: 1 if the actual address matches the legal address; 0 if it does not, in which case real_address is required.

// 200: the customer receives an SMS with the profile form link and a 6-digit code
{"token": "…64 characters", "message": "Success"}
// 402
{"message": "…validation text…"}
{"message": "ატვირთული დოკუმენტი არავალიდურია!"}   // complete verification/1–2 first

Every call sends a new SMS and revokes the previous code (see the note on the shared code). Limit: 5 calls per 10 minutes per customer.

verification-defect/3: confirms the filled-in data.

FieldRule
tokenThe token from step 2
sms6-digit code from the SMS
// 200
{"message": "Success"}
// 402
{"message": "sms-ით მიღებული კოდი არასწორია"}

After success, verification_defect in user becomes 0.

POST/api/v1/requested-documentcustomer token

Use it when user has requested_document: 1. APW has requested an image from the customer: which image is written in requested_document_comment.

FieldRule
fileThe image as a data URI: data:image/png;base64,…. JPEG, PNG or GIF, at most 10 MB (before base64).

This is a regular JSON request, not multipart. The signing rule is the same.

{"file": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ…"}

The response is always HTTP 200. Determine the result from errorCode:

errorCodeMeaning
0The image was uploaded
1Invalid file: not a base64 image, or the format or size does not match
2No image has been requested from this customer

ATM cash withdrawal (QR)

POST/api/v1/qr/verifycustomer token

The customer scans the QR code on the ATM screen and takes cash from the wallet. This endpoint binds the ATM session to the customer's wallet.

FieldRule
sessionIdRequired. The ATM session, from the QR code.
atmIdRequired. The ATM identifier, from the QR code.
// 200
{"errorCode": 0, "errorMessage": "Success"}

No SMS code is required: before calling, have the customer confirm in your application that they are standing at the ATM themselves. The request can take several minutes.

Response (HTTP 402)Meaning
errorCode 9The customer is blocked, not verified, or under an AML restriction
errorCode 8No valid document, or the QR data is invalid
other errorCodeThe ATM service's answer. The text is in errorMessage.
{"message": "..."}Field validation or a document check failure