SDKs
The SDKs below let your server verify Thai bank transfer slips and TrueMoney Wallet receipts with the bank, and confirm in the same request that a slip paid your account the right amount and was not used before. They are published under the EasySlip name (@easyslip/sdk, easyslip-sdk, easyslip/sdk and github.com/EASYSLIP-CO-LTD/sdk-go) and work with Thunder API v2 and v1 when you point them at the Thunder addresses, as shown below. They are available for JavaScript and TypeScript, Python, Go and PHP, and each one covers all of API v2 and API v1: slip verification, background jobs, receiving bank accounts, payment QR codes and webhook signature checks.
Each SDK adds your API key to every request, checks image files before it uploads them, returns a typed error for each kind of failure and retries failed requests without charging you twice for the same verification.
Choose your language
| Language | Requires |
|---|---|
| JavaScript and TypeScript | Node.js 22+ |
| Python | Python 3.11 to 3.15 |
| Go | Go 1.26+ |
| PHP | PHP 8.2+ |
Bun 1.4, Deno 2.9 and Cloudflare Workers also run the JavaScript SDK. The PHP SDK needs Guzzle 7.16 or later, or Guzzle 8.3 or later. The source code of each SDK is listed in Source code and license.
Each guide walks through every feature with programs you can run. The README in each repository is a shorter summary of the same SDK.
Installation
npm install @easyslip/sdkpython -m pip install easyslip-sdkgo get github.com/EASYSLIP-CO-LTD/sdk-gocomposer require easyslip/sdkUse the SDK with Thunder
The SDKs call the EasySlip addresses unless you tell them otherwise, and a Thunder API key is not valid there. Before you run any example, set both addresses to Thunder:
export EASYSLIP_V2_BASE_URL="https://api.thunder.in.th/v2"
export EASYSLIP_V1_BASE_URL="https://api.thunder.in.th/v1"You can pass the same addresses when you create the client instead: v2BaseUrl and v1BaseUrl in JavaScript and PHP, v2_base_url and v1_base_url in Python, and WithBaseURLs(v2, v1) in Go. Every request, response and error then matches this documentation, and the endpoints are the ones in API v2 and API v1.
Webhooks use a different header name on Thunder
Thunder signs a webhook with the X-Thunder-Signature header, while the SDKs name X-EasySlip-Signature in their WEBHOOK_SIGNATURE_HEADER and SIGNATURE_HEADER constants. Read X-Thunder-Signature yourself and pass its value to the SDK, as the webhook examples in each guide do. The signature check itself is the same: sha256= followed by the HMAC-SHA256 of the raw body.
Set up your API key
Sign in to the Thunder developer portal and copy the API key of your application. Authentication explains keys and branches. Then give the key to the SDK in one of these ways:
- Pass it when you create the client.
- Set the
EASYSLIP_API_KEYenvironment variable. The variable keeps the SDK's own name, but its value is your Thunder API key. - Save it in a file and set
EASYSLIP_API_KEY_FILEto the path of that file, for example a Docker or Kubernetes secret.
The SDK uses the first of these that is set.
export EASYSLIP_API_KEY="your-api-key"
# or keep the key in a file that only your app's user can read
export EASYSLIP_API_KEY_FILE="/run/secrets/easyslip-api-key"Keep your API key on your server
Anyone who has your key can use your quota. Use the SDK only in code that runs on your server. Never put the key in a web page or a mobile app, and never commit a key or a key file to a repository.
The SDK sends the key only in the Authorization header. If a response ever contains the key, the SDK removes it from error messages and from the headers it gives you, so you can log the SDK's errors.
The SDK also reads these variables:
| Variable | Default | What it changes |
|---|---|---|
EASYSLIP_V2_BASE_URL | https://api.easyslip.com/v2 | The address of API v2. Set it to https://api.thunder.in.th/v2 for Thunder. |
EASYSLIP_V1_BASE_URL | https://api.easyslip.com/v1 | The address of API v1. Set it to https://api.thunder.in.th/v1 for Thunder. |
EASYSLIP_WEBHOOK_SECRET | None | Your branch's webhook secret. Only the JavaScript SDK reads it for you. In the other languages, pass the secret to the webhook function. |
Verify your first slip
This program checks the slip a customer sent for an order of 1,500 baht. It asks the API whether the slip paid one of your registered bank accounts, whether the amount is right and whether the slip was used before, and accepts the payment only when all three checks pass:
import { EasySlip } from '@easyslip/sdk'
const easyslip = new EasySlip() // reads your key from EASYSLIP_API_KEY
const result = await easyslip.verify.bankByImage('./slip.jpg', {
matchAccount: true, // did the slip pay one of your bank accounts?
matchAmount: 1500, // is the amount right?
checkDuplicate: true, // was this slip used before?
})
const paid = result.isDuplicate === false && result.isAmountMatched === true && result.matchedAccount != null
console.log(paid ? `Paid: ${result.rawSlip.transRef}` : 'Do not accept this slip.')from easyslip import Client
from easyslip.models import MatchedAccount, VerifyOptions
with Client() as client: # reads your key from EASYSLIP_API_KEY
result = client.verify.bank_by_image(
"slip.jpg",
VerifyOptions(
match_account=True, # did the slip pay one of your bank accounts?
match_amount=1500.0, # is the amount right?
check_duplicate=True, # was this slip used before?
),
)
paid = (
result.is_duplicate is False
and result.is_amount_matched is True
and isinstance(result.matched_account, MatchedAccount)
)
print(f"Paid: {result.raw_slip.trans_ref}" if paid else "Do not accept this slip.")package main
import (
"context"
"fmt"
"log"
easyslip "github.com/EASYSLIP-CO-LTD/sdk-go"
)
func main() {
client, err := easyslip.NewClient("") // reads your key from EASYSLIP_API_KEY
if err != nil {
log.Fatal(err)
}
defer client.Close()
result, err := client.Verify.BankByImage(context.Background(), "slip.jpg", &easyslip.VerifyOptions{
MatchAccount: easyslip.Some(true), // did the slip pay one of your bank accounts?
MatchAmount: easyslip.Some(1500.0), // is the amount right?
CheckDuplicate: easyslip.Some(true), // was this slip used before?
})
if err != nil {
log.Fatal(err)
}
duplicate, checked := result.IsDuplicate.Value()
amountMatched, _ := result.IsAmountMatched.Value()
account, _ := result.MatchedAccount.Value()
if checked && !duplicate && amountMatched && account != nil {
fmt.Println("Paid:", result.RawSlip.TransRef)
} else {
fmt.Println("Do not accept this slip.")
}
}<?php
require __DIR__ . '/vendor/autoload.php';
use EasySlip\EasySlip;
use EasySlip\Input\FileInput;
use EasySlip\Input\VerifyOptions;
$easyslip = new EasySlip(); // reads your key from EASYSLIP_API_KEY
$result = $easyslip->verify->bankByImage(FileInput::fromPath(__DIR__ . '/slip.jpg'), new VerifyOptions(
matchAccount: true, // did the slip pay one of your bank accounts?
matchAmount: 1500, // is the amount right?
checkDuplicate: true, // was this slip used before?
));
$paid = $result->isDuplicate === false && $result->isAmountMatched === true && $result->matchedAccount !== null;
echo $paid ? "Paid: {$result->rawSlip->transRef}\n" : "Do not accept this slip.\n";matchAccount compares the receiver with the bank accounts you registered, so add your receiving accounts first, in the developer portal or with the SDK. Each verification uses one unit of your quota. The guide for your language shows the full version of this program, with a message for each failed check and the error for a slip the bank does not know. POST /verify/bank describes the request and the response.
Take the order total from your own database, never from the customer, and store transRef with a unique constraint when you mark the order paid, so that the same slip cannot pay two orders.
What each SDK covers
| Task | API | JavaScript | Python | Go | PHP |
|---|---|---|---|---|---|
| Account info and quota | GET /info | getInfo | get_info | GetInfo | getInfo |
| Verify a bank slip | POST /verify/bank | verify.bankBy* | verify.bank_by_* | Verify.BankBy* | verify->bankBy* |
| Verify a TrueMoney Wallet receipt | POST /verify/truewallet | verify.truewalletBy* | verify.truewallet_by_* | Verify.TruewalletBy* | verify->truewalletBy* |
| Background and batch verification | Async Verify | jobs.* | jobs.* | Jobs.* | jobs->* |
| Bank codes | GET /banks | banks.list | banks.list | Banks.List | banks->list |
| Receiving bank accounts | Bank Accounts | bankAccounts.* | bank_accounts.* | BankAccounts.* | bankAccounts->* |
| Payment QR codes | POST /qr/generate | v1.generateQr | v1.generate_qr | V1.GenerateQR | v1->generateQr |
| Legacy API v1 | API v1 | v1.* | v1.* | V1.* | v1->* |
| Check a webhook | Webhook Callback | constructWebhookEvent | webhooks.construct_event | ConstructWebhookEvent | Webhook::constructEvent |
By* names how you send the slip: Payload (the text in the slip's QR code), Image (a file or an upload), Base64 or URL. TrueMoney Wallet receipts have no payload method.
What every SDK does the same way
- Timeouts and retries. Each attempt may take 30 seconds, and a failed request is retried at most 2 times. You can change both when you create the client. The SDK waits about half a second before the first retry and doubles the wait each time, up to 8 seconds.
- No double charges. Verifications and background job submissions use quota, so the SDK retries them only when the API answers 429 or the connection failed before the request was sent. The same rule applies to creating, updating and unlinking a bank account. Reading data, linking an account and creating a QR code are safe to repeat, so they are also retried after a timeout, a network failure or a 500, 502, 503 or 504 answer. A 400, 401, 403, 404 or 409 answer is never retried.
- Retry-After. When the API sends
Retry-After, the SDK waits that long before it retries. If the wait would be longer than 60 seconds, the SDK returns the error at once. - File checks before upload. Images must be JPEG, PNG, GIF or WebP files of at most 4 MB. The SDK returns an
InputErrorfor an empty file, a file over 4 MB or a file that is not an image, without sending anything. - Request values. In a request, the receiving account type is
NATURALorJURISTIC, and the QR type is one of five values. TypeScript and Python type checkers reject other values through theAccountTypeandQrTypetypes, PHP takes theAccountTypeandQrTypeenums, and Go has constants such asAccountTypeJuristicandQRTypePromptPay. Fields in responses accept any string, so a value the API adds later does not break your code. - Typed errors. Every SDK uses the same error names, such as
SlipNotFoundError,QuotaExceededErrorandRateLimitError, and they all share the typeEasySlipError. In JavaScript, Python and PHP it is the base class of every error. In Go it is an interface, and you find the exact type witherrors.As. Error Codes explains each code the API returns. - Webhook checks. Each SDK checks the signature value you pass it, which comes from the
X-Thunder-Signatureheader, which holdssha256=followed by the HMAC-SHA256 of the raw body, with a constant time comparison. The webhook examples in each guide answer status 400 when the signature is missing or wrong and when a signed body is not a webhook event.
Source code and license
The source code of each SDK is on GitHub: sdk-js, sdk-python, sdk-go and sdk-php. Each repository has a README, a changelog and an examples folder with one runnable program for every method. Report bugs on the repository's Issues page, and never post your API key or a real slip there.
All four SDKs are released under the MIT License, Copyright (c) 2026 EasySlip Co., Ltd.
For questions about your account, quota or billing, contact Thunder Solution through the developer portal.
