PHP SDK

Use the official teyra/teyra-pay-php library to accept payments, paginate lists, retry safely and verify webhooks from PHP.

Coming soon

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-Key on every POST, reused across retries
  • Automatic retries on network errors, 429 and 5xx
  • 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-php

Quick 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

MethodEndpoint
$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().

ExceptionStatus
InvalidRequestException400, 422: validation_error, refund_exceeds_amount, payment_not_refundable
AuthenticationException401
PermissionException403: merchant_not_active, forbidden
NotFoundException404
ConflictException, IdempotencyException409
RateLimitException429 (getRetryAfter() in seconds)
ApiException5xx
ApiConnectionExceptionNetwork 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 places

Pass 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.