PHP SDK
Use the official teyra/teyra-pay-php library to accept payments, paginate lists, retry safely and verify webhooks from PHP.
Not published yet
teyra/teyra-pay-php is built and tested but not on Packagist yet, so composer require teyra/teyra-pay-php will not work until it is released. Contact support@teyra.co for early access. Until then, call the REST API directly as shown in the quick start.
teyra/teyra-pay-php is the official Teyra Pay client for PHP. It works on PHP 7.4 and later, follows PSR-4, uses cURL and has no required dependencies.
- Services for payments, refunds, payment links, deposits and webhook endpoints
- An auto-pagination iterator
- An
Idempotency-Keyon everyPOST, reused across retries - Automatic retries on network errors,
429and5xx - Typed exceptions with error code, HTTP status and details
- Money helpers that never use floats
- Webhook signature verification
Install
Requires PHP 7.4 or later with the curl and json extensions.
composer require teyra/teyra-pay-phpQuick start
Create a client with your secret key, create a payment and send the customer to checkout_url.
<?php
require __DIR__ . '/vendor/autoload.php';
use Teyra\Money;
use Teyra\TeyraPay;
$teyra = new TeyraPay(getenv('TEYRA_SECRET_KEY')); // sk_test_… or sk_live_…
$payment = $teyra->payments->create([
'amount' => Money::toMinor('12.500', 'KWD'), // 12500
'currency' => 'KWD',
'return_url' => 'https://shop.example.com/thanks',
'reference' => 'order-1042',
'customer' => ['name' => 'Sara Ali', 'email' => 'sara@example.com'],
'metadata' => ['order_id' => '1042'],
]);
header('Location: ' . $payment->checkout_url, true, 303);When the customer comes back to your return_url, confirm the result on your server. Don't trust the redirect alone:
$payment = $teyra->payments->retrieve($_GET['payment_id']);
if ($payment->status === 'succeeded') {
// mark the order as paid
}Responses are read-only Teyra\TeyraObjects. Read fields as properties or keys ($payment->id, $payment['status'], $payment->customer->email) or call $payment->toArray().
Keep the secret key on your server
Never expose sk_test_… or sk_live_… keys in HTML, JavaScript or a mobile app. Load them from an environment variable. See Authentication.
Client options
$teyra = new TeyraPay(getenv('TEYRA_SECRET_KEY'), [
'base_url' => 'https://api.teyra.co', // default
'timeout_ms' => 30000, // per attempt, default 30 seconds
'max_retries' => 2, // default 2
]);The key decides the mode: test keys create test objects (livemode: false) and live keys create live ones. See Test and live mode.
Methods
| Method | Endpoint |
|---|---|
$teyra->payments->create($params) | POST /v1/payments |
$teyra->payments->retrieve($id) | GET /v1/payments/:id |
$teyra->payments->list($params) | GET /v1/payments |
$teyra->refunds->create($paymentId, $params) | POST /v1/payments/:id/refunds |
$teyra->refunds->retrieve($id) | GET /v1/refunds/:id |
$teyra->refunds->list($params) | GET /v1/refunds |
$teyra->paymentLinks->create($params) | POST /v1/payment-links |
$teyra->paymentLinks->retrieve($id) | GET /v1/payment-links/:id |
$teyra->paymentLinks->list($params) | GET /v1/payment-links |
$teyra->paymentLinks->deactivate($id) | POST /v1/payment-links/:id/deactivate |
$teyra->deposits->list($params) | GET /v1/deposits |
$teyra->webhookEndpoints->create($params) | POST /v1/webhook-endpoints |
$teyra->webhookEndpoints->list() | GET /v1/webhook-endpoints |
$teyra->webhookEndpoints->delete($id) | DELETE /v1/webhook-endpoints/:id |
Parameters and responses match the API reference. Every method also accepts request options as its last argument: ['idempotency_key' => …, 'timeout_ms' => …, 'max_retries' => …].
// Partial refund
$refund = $teyra->refunds->create('pay_01JB…', ['amount' => 5000, 'reason' => 'requested_by_customer']);
// Shareable payment link
$link = $teyra->paymentLinks->create(['amount' => 5000, 'currency' => 'KWD', 'description' => 'Consultation']);
echo $link->url;Pagination
list() returns a Teyra\Collection with one page. foreach over it iterates that page. autoPagingIterator() walks every page and fetches the next one with next_cursor only when you reach it, keeping your filters.
// One page
$page = $teyra->payments->list(['status' => 'succeeded', 'limit' => 50]);
count($page); // items on this page
$page->hasMore(); // bool
$page->getNextCursor(); // ?string
// Every item, across pages
foreach ($teyra->payments->list(['status' => 'succeeded'])->autoPagingIterator() as $payment) {
echo $payment->id, ' ', $payment->amount, PHP_EOL;
}
// Page by page
foreach ($teyra->deposits->list(['limit' => 100])->pages() as $page) {
echo count($page), PHP_EOL;
}See Pagination for how cursors work.
Idempotency
The SDK sends an Idempotency-Key header with every POST. If you don't pass one, it generates a UUID and reuses it on every retry of that call, so a retried request can't create a second payment or refund.
When the same operation might run again later, for example from a queued job or a double-submitted form, pass your own key derived from your order:
$teyra->payments->create($params, ['idempotency_key' => 'order-1042-attempt-1']);
$teyra->refunds->create('pay_01JB…', ['amount' => 5000], ['idempotency_key' => 'refund-order-1042-item-3']);Reusing a key with a different body throws IdempotencyException (409 idempotency_conflict). See Idempotency.
Errors and retries
Every exception extends Teyra\Exception\TeyraPayException, which has getErrorCode(), getHttpStatus(), getMessage() and getDetails().
| Exception | Status |
|---|---|
InvalidRequestException | 400, 422: validation_error, refund_exceeds_amount, payment_not_refundable |
AuthenticationException | 401 |
PermissionException | 403: merchant_not_active, forbidden |
NotFoundException | 404 |
ConflictException, IdempotencyException | 409 |
RateLimitException | 429 (getRetryAfter() in seconds) |
ApiException | 5xx |
ApiConnectionException | Network error or timeout (no HTTP status) |
use Teyra\Exception\InvalidRequestException;
use Teyra\Exception\TeyraPayException;
try {
$teyra->refunds->create('pay_01JB…', ['amount' => 999999]);
} catch (InvalidRequestException $e) {
echo $e->getErrorCode(); // e.g. refund_exceeds_amount
print_r($e->getDetails()); // e.g. ['fields' => ['amount' => '…']] on validation_error
} catch (TeyraPayException $e) {
error_log($e->getHttpStatus() . ' ' . $e->getErrorCode() . ' ' . $e->getMessage());
}Network errors, timeouts, 429 and 5xx responses are retried up to max_retries times (default 2) with exponential backoff of 0.5 s, 1 s, 2 s and so on, up to 8 s. When the API sends Retry-After, the SDK waits that long instead. Other 4xx errors are never retried. See Errors for every code.
Money helpers
use Teyra\Money;
Money::toMinor('1.500', 'KWD'); // 1500
Money::toMinor('19.99', 'AED'); // 1999
Money::fromMinor(1500, 'KWD'); // '1.500'
Money::fromMinor(2500, 'SAR'); // '25.00'
Money::exponent('BHD'); // 3
Money::toMinor('1.2345', 'KWD'); // throws: KWD amounts have at most 3 decimal placesPass amounts as strings for exact values. See Money and currencies.
Verify webhooks
Webhook::constructEvent($payload, $sigHeader, $secret, $tolerance = 300) checks the Teyra-Signature header and returns the parsed event. It throws SignatureVerificationException when the header is missing, the timestamp is more than $tolerance seconds away, or no signature matches. Pass the raw body, before json_decode.
<?php
use Teyra\Exception\SignatureVerificationException;
use Teyra\Webhook;
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_TEYRA_SIGNATURE'] ?? null;
try {
$event = Webhook::constructEvent($payload, $header, getenv('TEYRA_WEBHOOK_SECRET'));
} catch (SignatureVerificationException $e) {
http_response_code(400);
exit('Invalid signature');
}
if ($event->type === 'payment.succeeded') {
mark_order_paid($event->data->object->id);
}
http_response_code(200);Exclude the webhook route from CSRF protection. Webhook::generateTestHeader($payload, $secret) builds a valid header for your own tests. See Verify webhook signatures and Webhooks.
Try the API in Postman
Prefer to click through requests first? Import the Postman collection.