omise/omise-php
Official PHP client library for the Omise payment gateway. Charge cards, manage customers, refunds, transfers, and webhooks through a clean, object-oriented API that wraps Omise's REST endpoints. Installed via Composer and framework-agnostic, so it drops easily into Laravel or any PHP application.
Install it and read two files — the whole API surface is there:
composer require omise/omise-php
lib/Omise.php — bootstrap/configuration hook (key handling differs between v2 and v3).lib/OmiseApiResource.php — the base class every resource inherits (retrieve, create, update, destroy, list helpers, HTTP client injection, API version pinning).Everything else in lib/Omise/ is a ~50-line resource class (Charge.php, Customer.php, Token.php, Source.php, Refund.php, Dispute.php, Transfer.php…) that mostly just declares its endpoint path. So the "first use case" is always the same shape: configure keys → build a token/charge → inspect status.
Minimum viable boot (defensive across versions, because key setup changed between releases):
final class OmiseBootstrap
{
public function __construct(string $public, string $secret, ?string $version = null)
{
if (method_exists(\Omise::class, 'initialize')) {
\Omise::initialize($public, $secret, $version); // v3+
return;
}
// Legacy: constants are read lazily on the first API call.
defined('OMISE_PUBLIC_KEY') || define('OMISE_PUBLIC_KEY', $public);
defined('OMISE_SECRET_KEY') || define('OMISE_SECRET_KEY', $secret);
defined('OMISE_API_VERSION') || define('OMISE_API_VERSION', $version ?? '2019-05-29');
}
}
First charge, using a token minted client-side with omise.js (the only PCI-sane path):
$charge = OmiseCharge::create([
'amount' => 100_00, // smallest currency unit, integer
'currency' => 'thb',
'card' => $token, // 'tokn_test_...' from the browser
]);
if ($charge['status'] === 'successful') {
// fulfil the order, keyed by $charge['id']
}
$charge is an OmiseObject — ArrayAccess plus __get, so $charge['status'] and $charge->status both work.
Get test keys from the Omise/Opn dashboard, set OMISE_PUBLIC_KEY / OMISE_SECRET_KEY in .env, and keep the secret server-side only.
Bind it once, wrap it, never call statics from controllers. There is no Laravel service provider or facade in this package — you write the ~40 lines of glue.
// config/omise.php
return [
'public_key' => env('OMISE_PUBLIC_KEY'),
'secret_key' => env('OMISE_SECRET_KEY'),
'api_version' => env('OMISE_API_VERSION', '2019-05-29'),
];
// App\Providers\AppServiceProvider::register()
$this->app->singleton(OmiseBootstrap::class, fn () => new OmiseBootstrap(
config('omise.public_key'), config('omise.secret_key'), config('omise.api_version'),
));
$this->app->bind(PaymentGateway::class, OmiseGateway::class);
Then put every SDK call behind your own PaymentGateway interface that returns your DTOs (ChargeResult, RefundResult), not OmiseObject. This is what makes the SDK swappable and testable — you fake the interface, not cURL.
Token charge (default flow). Browser does Omise.createToken('card', {...}) with the public key; server does OmiseCharge::create(['card' => $token, ...]). Your server never touches a PAN.
Customer + saved card. Create the customer with the token, charge later with customer + card:
OmiseCustomer::create(['email' => $email, 'description' => $user->id, 'card' => $token]);
OmiseCharge::create([
'amount' => $amount,
'currency' => 'thb',
'customer' => $customerId, // 'cust_test_...'
'card' => $cardId, // 'card_test_...'
]);
Card retrieve takes both ids (OmiseCard::retrieve($customerId, $cardId)) — check lib/Omise/Card.php for the exact signature in your version.
Sources for local payment methods (PromptPay, Alipay, TrueMoney, FPX, etc.):
$source = OmiseSource::create([
'type' => 'promptpay', 'amount' => $amount, 'currency' => 'thb',
]);
$charge = OmiseCharge::create([
'amount' => $amount, 'currency' => 'thb', 'source' => $source['id'],
]);
Then redirect the user to $charge['authorize_uri'] (QR image / bank page) and wait for the webhook.
Auth-only + capture. Pass 'capture' => false; the charge comes back authorized. Capture with $charge->capture() (or OmiseCharge::capture($id)), void instead if you need to release. Do it before the authorization window lapses.
Refunds. $charge->refund(['amount' => $partial]), or OmiseRefund::create($chargeId, [...]). Track cumulative refunds yourself against $charge['refunds'] — the API will reject over-refunds, but you want a friendly error first.
Webhooks in Laravel. Omise does not sign the payload in the classic API, so treat the body as a hint only:
Route::post('/webhooks/omise', OmiseWebhookController::class)
->withoutMiddleware([VerifyCsrfToken::class]);
public function __invoke(Request $request)
{
$event = OmiseEvent::retrieve($request->input('key')); // authoritative re-fetch
ProcessOmiseEvent::dispatch($event['id'])->onQueue('payments');
return response()->noContent();
}
Return 2xx fast, do the work in a job, and make the handler idempotent keyed on $charge['id'] (webhooks retry).
Testing. If your version exposes an injectable HTTP client (OmiseApiResource::$httpClient / setHttpClient() in v3), stub that. Otherwise the interface-wrapper above is the practical seam. Keep one sandbox integration test that hits the real test API.
10000. Do not blanket-multiply by 100: JPY, KRW and friends are zero-decimal in Omise. Use Money::toMinor() and store integers everywhere; currency must be lowercase ISO-4217.status: 'failed' and a failure_code / failure_message. Always branch on status (successful, failed, pending, reversed, expired, authorized) before writing your own catch.OmiseAuthenticationFailureException, OmiseNotFoundException, OmiseBadRequestException, OmiseApiConnectionError, etc. Map them in app/Exceptions/Handler.php (or your gateway) so a gateway blip doesn't 500 the checkout page.pending ≠ successful. 3-D Secure and redirect sources leave status: pending with an authorize_uri. Redirect the user, then confirm via webhook — never treat the initial create response as payment.define()d once.Idempotency-Key before relying on the SDK.OMISE_API_VERSION. The SDK default can lag the API; an unpinned upgrade silently changes response shapes.declare(strict_types=1) in your code, convert OmiseObjects into DTOs at the boundary, and don't type-hint against SDK classes in your domain.json_encode($charge) may not give you what you expect — nested values are themselves OmiseObjects. Convert explicitly (recursively) before logging or caching.OmiseApiConnectionError from TLS failures, not auth errors. Inject a custom HTTP client if you need proxy settings or request tracing..env copy-paste from charging real cards in staging.OmiseApiResource::$httpClient / setHttpClient() for instrumentation and retries, OmiseApiResource subclassing for custom resources, and the resource classes' static retrieve() (with no id) plus limit/offset/from/to for list endpoints and reconciliation jobs.OmiseDispute::update() with documents — plan a UI for it early; the dispute window is short.How can I help you explore Laravel packages today?