PHP SDK
Use the official OrcaRail PHP SDK to accept crypto payments from any PHP application. The SDK handles Basic authentication, JSON request/response formatting, and webhook signature verification.
The client follows the modern Stripe PHP style: one OrcaRailClient, lazily created service properties, associative arrays for request parameters, and forward-compatible response objects.
Requirements
- PHP 8.1 or newer
ext-curlandext-json
Installation
Install the package with Composer:
composer require orcarail/orcarail-php
The package name is orcarail/orcarail-php and it autoloads the OrcaRail\ namespace (PSR-4). To install from a repository checkout instead, add a path or vcs repository to your composer.json:
{
"repositories": [{ "type": "path", "url": "../orcarail/sdks-php" }],
"require": { "orcarail/orcarail-php": "*" }
}
Configuration
Create a client with your API key and secret. Optionally set a custom base URL (e.g. for local development) and timeouts.
<?php
require __DIR__ . '/vendor/autoload.php';
use OrcaRail\OrcaRailClient;
$orcarail = new OrcaRailClient([
'api_key' => getenv('ORCARAIL_API_KEY'), // e.g. "ak_live_xxx"
'api_secret' => getenv('ORCARAIL_API_SECRET'), // e.g. "sk_live_xxx"
'base_url' => 'https://api.orcarail.com/api/v1', // optional; default
'timeout' => 30000, // optional; milliseconds, default 30000
'connect_timeout' => 10000, // optional; milliseconds, default 10000
]);
For local development, point base_url to your API:
$orcarail = new OrcaRailClient([
'api_key' => getenv('ORCARAIL_API_KEY'),
'api_secret' => getenv('ORCARAIL_API_SECRET'),
'base_url' => 'http://127.0.0.1:3000/api/v1',
'timeout' => 65000, // /complete can take up to ~65s
]);
Services are created on first access and reused: $orcarail->paymentIntents, $orcarail->subscriptions, $orcarail->pay, $orcarail->rates, $orcarail->products, and $orcarail->prices.
Response objects
Every JSON object in a response becomes an OrcaRail\OrcaRailObject. Unknown fields are preserved, so new API fields are readable without upgrading the SDK. Objects support property access, array access, iteration, count(), toArray(), and json_encode().
$intent = $orcarail->paymentIntents->retrieve($intentId);
echo $intent->status; // property access
echo $intent['client_secret']; // array access
echo $intent->payment_link->link; // nested object
$asArray = $intent->toArray(); // plain PHP array
Missing keys return null instead of raising a warning, so check before use:
if ($intent->payment_link !== null) {
header('Location: ' . $intent->payment_link->link);
}
Commission (split and get your share)
API keys can have an optional commission percent (0–100). Set it in the Dashboard → API Keys or via PATCH /api/v1/api-keys/:id with commissionPercent. Payments created with that key include the commission in the quote (visible to the payer), and the commission amount is sent to your withdrawal settings address when funds are withdrawn.
Payment Intents
Create a Payment Intent
Create a payment intent with either a catalog price_id or the full set of direct amount parameters (amount, currency, tokenId, networkId). The two forms are mutually exclusive; the SDK rejects a mix before sending the request.
$intent = $orcarail->paymentIntents->create([
'amount' => '100.00',
'currency' => 'usd',
'tokenId' => '550e8400-e29b-41d4-a716-446655440000', // token UUID
'networkId' => '3f1a0c1e-9d7b-4f8a-9d21-4a2f6b7c8d90', // network UUID
'return_url' => 'https://yourapp.com/success',
'cancel_url' => 'https://yourapp.com/cancel',
'description' => 'Order #12345',
'metadata' => ['order_id' => '12345'],
// optional: override where funds are withdrawn, keyed by chain family
'withdrawal_addresses' => ['evm' => '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'],
]);
echo $intent->id; // e.g. "550e8400-e29b-41d4-a716-446655440000"
echo $intent->client_secret;
echo $intent->payment_link?->link;
payment_method_types defaults to ['crypto'] when you omit it. Use Payments: List Networks and Payments: List Tokens by Network to get the network and token UUIDs.
With a catalog price, pass price_id alone:
$intent = $orcarail->paymentIntents->create([
'price_id' => $priceId,
'return_url' => 'https://yourapp.com/success',
]);
Confirm and redirect to pay
Confirm the intent to get the hosted pay URL, then redirect the customer. Confirmation is authenticated with your API key and verifies the client secret.
$confirmed = $orcarail->paymentIntents->confirm($intent->id, [
'client_secret' => $intent->client_secret,
'return_url' => 'https://yourapp.com/success',
]);
$payUrl = $confirmed->pay_url ?? $confirmed->next_action?->redirect_to_url?->url;
header('Location: ' . $payUrl);
Retrieve a Payment Intent
$intent = $orcarail->paymentIntents->retrieve('550e8400-e29b-41d4-a716-446655440000');
echo $intent->status;
Update a Payment Intent
Update amount, metadata, URLs, or withdrawal addresses before the customer pays:
$updated = $orcarail->paymentIntents->update('550e8400-e29b-41d4-a716-446655440000', [
'amount' => '150.00',
'metadata' => ['order_id' => '12345'],
'withdrawal_addresses' => ['evm' => '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'],
]);
Cancel a Payment Intent
$canceled = $orcarail->paymentIntents->cancel('550e8400-e29b-41d4-a716-446655440000');
Complete a Payment Intent
When the customer lands on your success/return URL, mark the intent as processing (this triggers the payment_intent.processing webhook):
$completed = $orcarail->paymentIntents->complete('550e8400-e29b-41d4-a716-446655440000');
Pay by slug
If you use payment links with slugs, read pay details and cancel by slug:
$pay = $orcarail->pay->retrieve('pl_abc123'); // get() is an alias
$orcarail->pay->cancel('pl_abc123');
Subscriptions
Subscription collection routes infer the organization from the authenticated API key. Create with either price_id or the full direct parameter set (amount, currency, token_id, network_id, interval) — note the snake_case field names here, matching the REST contract.
$subscription = $orcarail->subscriptions->create([
'description' => 'Monthly Pro Plan',
'amount' => '10.00',
'currency' => 'usd',
'token_id' => $tokenId,
'network_id' => $networkId,
'interval' => 'month',
'interval_count' => 1,
'collection_method' => 'send_payment_link',
'total_cycles' => 12,
'payer_email' => '[email protected]',
'metadata' => ['plan_id' => 'pro_monthly'],
]);
echo $subscription->id;
echo $subscription->current_period_end;
echo $subscription->latest_payment_link?->link;
List, update, cancel, and resume:
$page = $orcarail->subscriptions->all([
'status' => 'active',
'limit' => 20,
'current_period_end' => ['lte' => '2026-12-31T00:00:00.000Z'], // sent as current_period_end[lte]
]);
foreach ($page->data as $sub) {
echo $sub->id . ' ' . $sub->description . PHP_EOL;
}
echo $page->has_more ? 'more pages' : 'last page';
$orcarail->subscriptions->update($subscription->id, ['cancel_at_period_end' => true]);
$orcarail->subscriptions->cancel($subscription->id, [
'cancellation_details' => ['comment' => 'Switching to annual', 'feedback' => 'other'],
]);
$orcarail->subscriptions->resume($subscription->id);
$links = $orcarail->subscriptions->listPaymentLinks($subscription->id, ['limit' => 10]);
The method is all() rather than list() because list is a reserved word in PHP.
Payer-facing auto-charge calls (approve-auto-charge, revoke-auto-charge, set-collection-method) are not part of the SDK — call them over HTTP without an API key from your pay UI. See Auto-charge.
Catalog: products and prices
Catalog routes are organization-scoped, so pass the organization UUID as the first argument.
$products = $orcarail->products->all($organizationId);
$product = $orcarail->products->create($organizationId, [
'name' => 'Pro Plan',
'description' => 'Monthly access',
'active' => true,
]);
$prices = $orcarail->prices->all($organizationId, ['active' => true]);
$recurring = $orcarail->prices->listActiveRecurring($organizationId);
// Find or create a one-time price for a fixed amount (idempotent by amount + currency + product name)
$price = $orcarail->prices->ensureOneTime($organizationId, [
'amount' => '25.00',
'currencyCode' => 'usd',
'tokenId' => $tokenId,
'networkId' => $networkId,
'productName' => 'One-time checkout',
]);
$intent = $orcarail->paymentIntents->create([
'price_id' => $price->id,
'return_url' => 'https://yourapp.com/success',
]);
products->all() and prices->all() unwrap the API's list envelope and return arrays of OrcaRailObject.
Rates
$quote = $orcarail->rates->getFiatQuote(['amount' => '100.00', 'currency' => 'eur']);
echo $quote->amountUsd;
$currencies = $orcarail->rates->getCurrencies(['active' => true]);
foreach ($currencies as $currency) {
echo $currency->code . PHP_EOL;
}
See Price API for response fields.
Webhooks
Webhook helpers are static and do not need a client, because verification only requires the raw body, the x-webhook-signature header, and your webhook secret.
Verify a signature
use OrcaRail\WebhookSignature;
$payload = file_get_contents('php://input'); // raw body, never a re-encoded array
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
if (!WebhookSignature::verifyHeader($payload, $signature, $webhookSecret)) {
http_response_code(400);
echo 'Invalid signature';
exit;
}
Construct an event (verify and parse)
use OrcaRail\Enum\WebhookEventType;
use OrcaRail\Exception\SignatureVerificationException;
use OrcaRail\Webhook;
try {
$event = Webhook::constructEvent(
file_get_contents('php://input'),
$_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
getenv('ORCARAIL_WEBHOOK_SECRET'),
);
} catch (SignatureVerificationException $exception) {
http_response_code(400);
exit;
}
$object = $event->data->object;
$type = WebhookEventType::tryFrom((string) $event->type);
match ($type) {
WebhookEventType::PaymentIntentCompleted => fulfilOrder($object->id),
WebhookEventType::PaymentIntentProcessing => markProcessing($object->id),
WebhookEventType::PaymentIntentCanceled => releaseOrder($object->id),
WebhookEventType::SubscriptionPaymentLinkPaid => creditCycle($object->id),
// tryFrom() returns null for event types this SDK version does not know
default => null,
};
http_response_code(200);
echo json_encode(['received' => true]);
constructEvent() throws SignatureVerificationException for a bad signature, malformed JSON, or a body that is not a JSON object. Always answer 200 only after you have accepted the event.
Laravel example
Read the raw request body — a parsed array will not verify.
use Illuminate\Http\Request;
use OrcaRail\Exception\SignatureVerificationException;
use OrcaRail\Webhook;
Route::post('/webhooks/orcarail', function (Request $request) {
try {
$event = Webhook::constructEvent(
$request->getContent(),
$request->header('x-webhook-signature', ''),
config('services.orcarail.webhook_secret'),
);
} catch (SignatureVerificationException) {
return response('Invalid signature', 400);
}
// Dispatch a job keyed by $event->data->object->id, then acknowledge.
return response()->json(['received' => true]);
});
Exclude the route from CSRF verification, and make sure no middleware rewrites the body before verification.
Symfony example
use OrcaRail\Exception\SignatureVerificationException;
use OrcaRail\Webhook;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class OrcaRailWebhookController
{
#[Route('/webhooks/orcarail', methods: ['POST'])]
public function __invoke(Request $request): Response
{
try {
$event = Webhook::constructEvent(
$request->getContent(),
$request->headers->get('x-webhook-signature', ''),
$_ENV['ORCARAIL_WEBHOOK_SECRET'],
);
} catch (SignatureVerificationException) {
return new Response('Invalid signature', Response::HTTP_BAD_REQUEST);
}
return new JsonResponse(['received' => true]);
}
}
Error handling
The SDK throws typed exceptions that all extend OrcaRail\Exception\OrcaRailException (itself a RuntimeException).
use OrcaRail\Exception\ApiException;
use OrcaRail\Exception\AuthenticationException;
use OrcaRail\Exception\OrcaRailException;
use OrcaRail\Exception\TransportException;
try {
$intent = $orcarail->paymentIntents->create([
'price_id' => $priceId,
'return_url' => 'https://yourapp.com/success',
]);
} catch (AuthenticationException $exception) {
// 401 — invalid API key or secret
error_log($exception->getMessage());
} catch (ApiException $exception) {
// Any other non-2xx response
error_log($exception->statusCode . ' ' . $exception->type . ' ' . $exception->getMessage());
error_log(json_encode($exception->request)); // method and URL of the failed call
} catch (TransportException $exception) {
// Network failure, timeout, or unparseable response
error_log($exception->getMessage());
} catch (OrcaRailException $exception) {
error_log($exception->getMessage());
}
Invalid parameter combinations (for example price_id together with amount) throw InvalidArgumentException before any HTTP request is made.
Testing without the network
Pass your own transport as http_client to record requests in tests. Any class implementing OrcaRail\HttpClient\ClientInterface works, and no API key is required.
use OrcaRail\HttpClient\ClientInterface;
use OrcaRail\OrcaRailClient;
$recorder = new class implements ClientInterface {
/** @var list<array{string, string, ?array<string, mixed>, bool}> */
public array $calls = [];
public function request(string $method, string $path, ?array $body = null, bool $requireAuth = true): mixed
{
$this->calls[] = [$method, $path, $body, $requireAuth];
return ['id' => 'pi_test', 'status' => 'requires_confirmation'];
}
};
$orcarail = new OrcaRailClient(['http_client' => $recorder]);
$intent = $orcarail->paymentIntents->create([
'price_id' => 'price_test',
'return_url' => 'https://example.com/return',
]);
// $recorder->calls[0] === ['POST', 'payment_intents', [...], true]
See Also
- PHP SDK API Reference - All classes, services, and methods
- Node.js SDK - Equivalent SDK for JavaScript and TypeScript
- Payment Intents API - REST reference
- Webhooks - Events and configuration
- Getting Started - Account and API keys