checkslip / sdk
Official PHP SDK for the Checkslip API: look up Thai bank transfers from a slip QR or image with the sending bank.
Requires
- php: ^8.2
- ext-json: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-client-implementation: ^1.0
- psr/http-factory: ^1.0
- psr/http-factory-implementation: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- guzzlehttp/guzzle: ^7.8
- php-http/mock-client: ^1.6
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^10.5 || ^11.0
- symfony/http-client: ^6.4 || ^7.0
Suggests
- guzzlehttp/guzzle: A PSR-18 HTTP client; the SDK applies the timeout option to it.
- symfony/http-client: A PSR-18 HTTP client (with nyholm/psr7); the SDK applies the timeout option to it.
Provides
None
Conflicts
None
Replaces
None
README
The official PHP client for the Checkslip API.
Checkslip looks up a Thai bank transfer with the sending bank. You send the text of the QR code on a slip, or a photo of the slip, and Checkslip returns what the bank holds for that transfer: amount, time, sender and receiver. Whether to accept the payment is your decision, made against your own order.
The package needs PHP 8.2 or later and any PSR-18 HTTP client.
Install
composer require checkslip/sdk
If your project has no PSR-18 client yet, add one. Guzzle is the common choice:
composer require checkslip/sdk guzzlehttp/guzzle
The client finds an installed client and factories through php-http/discovery, so there is nothing to wire up.
Quick start
Create an API key in the Checkslip web app and keep it on your server.
use Checkslip\Client; use Checkslip\Model\Slip; $checkslip = new Client(apiKey: getenv('CHECKSLIP_API_KEY'));
Look up a slip by its QR text
Read the QR code on the slip with any QR reader and send the text.
$check = $checkslip->slips->lookup( slip: '00330006000001010306902126302105340875102TH9104F86F', clientReference: 'ORDER-10023', // optional, your own id ); if ($check->isFound() && $check->slip instanceof Slip) { echo $check->slip->amount->toBaht(), " THB\n"; echo 'from ', $check->slip->sender->nameEn, ' at ', $check->slip->sender->bank->code, "\n"; echo 'to ', $check->slip->receiver->nameEn, ' ', $check->slip->receiver->account, "\n"; }
Amounts come back in satang (100 satang = 1 baht); toBaht() formats them. Account numbers come back as their last 4 digits.
Look up a slip from an image
Pass an Upload instead of the QR text. JPEG, PNG and WebP up to 10 MB are accepted. Checkslip reads the QR code from the image and does not store the image.
use Checkslip\Upload; $check = $checkslip->slips->lookup(slip: Upload::fromPath('/tmp/slip.jpg'));
From an uploaded file in a request:
$check = $checkslip->slips->lookup( slip: Upload::fromString($contents, filename: 'slip.jpg', contentType: 'image/jpeg'), );
Pass the QR text or an image, not both.
Outcomes
$check->outcome is one of:
| Outcome | Meaning | Credit used |
|---|---|---|
FOUND |
The bank returned the transfer. Details are in $check->slip. |
1 |
NOT_FOUND |
The bank has no transfer with this reference. | 0 |
DUPLICATE |
This account already looked up this transfer. $check->duplicateOf points at the earlier one. |
0 |
PENDING |
The bank has not answered yet. Checkslip keeps asking and sends a slip.checked webhook when it has the result. $check->retryAfter says how long to wait before reading it again with slips->get(). |
0 |
These are normal results, so the SDK returns them. Problems with the request itself, such as an unreadable QR code, come back as exceptions.
Errors
Every exception extends Checkslip\Exception\CheckslipException, which carries the HTTP $status, the problem $code, the $requestId to quote to support, and the full RFC 9457 problem body.
use Checkslip\Exception\InsufficientCreditException; use Checkslip\Exception\RateLimitException; use Checkslip\Exception\UnprocessableEntityException; use Checkslip\Exception\CheckslipException; try { $check = $checkslip->slips->lookup(slip: $qr); } catch (UnprocessableEntityException $e) { // The text is not a slip QR code, the QR was altered, or the slip is too old. // $e->code is INVALID_QR, QR_TAMPERED, TOO_OLD or INVALID_FILE. No credit used. } catch (InsufficientCreditException $e) { // Top up credits. } catch (RateLimitException $e) { echo "retry in {$e->retryAfter} s\n"; } catch (CheckslipException $e) { error_log("{$e->status} {$e->code} {$e->requestId} {$e->getMessage()}"); throw $e; }
| Exception | When |
|---|---|
BadRequestException |
400 |
AuthenticationException |
401, the API key is missing, wrong or revoked |
InsufficientCreditException |
402, NO_CREDIT or CREDIT_EXPIRED |
PermissionDeniedException |
403, the key lacks the scope this call needs |
NotFoundException |
404 |
ConflictException |
409 |
GoneException |
410, the resource is gone for good, such as an expired export |
UnprocessableEntityException |
422, and 413 or 415 for a bad upload |
RateLimitException |
429, with $retryAfter in seconds |
ServerException |
5xx other than 503 |
ServiceUnavailableException |
503, up but temporarily unable to serve |
ConnectionException |
no response arrived |
UnexpectedResponseException |
the response was not the shape the API documents |
WebhookVerificationException |
a webhook signature or timestamp did not check out |
Retries
The client retries on its own, up to maxRetries times (default 2), waiting 0.5 s, then 1 s, then 2 s and so on with random jitter. It only retries when repeating the request cannot do anything twice: 429 and 408, 5xx and connection failures on GET, PUT and DELETE, and anything carrying an Idempotency-Key. When the response has Retry-After, the client waits that long.
Turn it off with new Client(apiKey: $key, maxRetries: 0).
Idempotency
slips->lookup() always sends an Idempotency-Key. A repeat with the same key returns the first result and never uses a second credit. If you do not pass one, the SDK generates a key and reuses it for that call's retries only.
If your process can crash and run the same job again, pass a key built from your own data so the second run gets the first result:
$checkslip->slips->lookup(slip: $qr, idempotencyKey: "order-{$order->id}-slip-1");
Pagination
Every list has a pair of methods. listAll() walks every page and yields each item, following the cursor for you:
foreach ($checkslip->slips->listAll(outcome: [Outcome::Found]) as $record) { echo $record->requestId, "\n"; }
list() gives you one page at a time:
$page = $checkslip->slips->list(transRef: '630210534087'); foreach ($page as $record) { echo $record->requestId, "\n"; } while ($page = $page->nextPage()) { // ... }
To resume later, store $page->nextCursor and pass it back as cursor.
Webhooks
Checkslip sends a slip.checked message to your endpoints when a lookup finishes, including lookups that were PENDING. Messages are signed with the Standard Webhooks scheme. Check the signature before you trust a message:
use Checkslip\Webhook\Webhook; use Checkslip\Exception\WebhookVerificationException; $webhook = new Webhook(getenv('CHECKSLIP_WEBHOOK_SECRET')); try { $event = $webhook->verify($rawRequestBody, $request->getHeaders()); if ($event->type === EventType::SlipChecked) { $data = $event->slipChecked(); // $data->requestId, $data->outcome, $data->clientReference } http_response_code(204); } catch (WebhookVerificationException $e) { http_response_code(400); }
With a PSR-7 server request, verifyRequest($request) reads the body and headers for you.
Pass the raw body, before any JSON decoding. Checkslip retries a message that does not get a 2xx answer, and every retry has the same webhook-id, so store the ids you have handled and skip repeats.
After you rotate a secret, the old one works for 24 hours and each message carries two signatures. Pass both during the switch: new Webhook([$newSecret, $oldSecret]).
What the client covers
| Resource | Methods |
|---|---|
$client->slips |
lookup, get, list, listAll, counts, and exports (create, get, downloadPart) |
$client->banks |
list |
$client->billing |
balance->get, info (get, update), packages->list, ledger (list, listAll), orders (list, listAll, get, sync), receipts (list, listAll, get) |
$client->usage |
get |
$client->audit |
events (list, listAll, get), requests (list, listAll, get), exports (list, listAll, create, get, download) |
$client->webhooks |
verify, eventTypes->list, endpoints (list, listAll, create, get, update, delete, recover, rotateSecret, sendTest), deliveries (list, listAll, get, redeliver) |
The same names, in the same shape, are in the JavaScript, Go and Rust clients.
Downloads return a PSR-7 StreamInterface, so a large CSV or evidence file does not have to fit in memory.
Configuration
$checkslip = new Client( apiKey: 'csk_...', // default: CHECKSLIP_API_KEY in the environment baseUrl: 'https://api.checkslip.com', // default maxRetries: 2, // default timeout: 30.0, // seconds per attempt, applied to Guzzle and Symfony clients httpClient: $myPsr18Client, // default: discovered );
Links
- Documentation: https://checkslip.com/docs
- API reference: https://checkslip.com/en/docs/reference
- Other SDKs: JavaScript, Go, Rust