chuckbartowski / ovh-sdk
PHP SDK for the OVHcloud API — native request signing, typed clients and high-level modules for every product
Requires
- php: >=8.2
- symfony/http-client: ^6.4|^7.0
- symfony/http-client-contracts: ^3.0
Requires (Dev)
- phpunit/phpunit: ^10.5|^11.0
- symfony/config: ^6.4|^7.0
- symfony/dependency-injection: ^6.4|^7.0
- symfony/http-kernel: ^6.4|^7.0
Suggests
- symfony/config: To register OvhSdkBundle in a Symfony application
- symfony/dependency-injection: To register OvhSdkBundle in a Symfony application
- symfony/http-kernel: To register OvhSdkBundle in a Symfony application
README
☁️ OVHcloud SDK for PHP
A modern, fully typed PHP SDK for the entire OVHcloud API — every product, native request signing, no wrapper dependency.
Domains & DNS · Public Cloud · Dedicated · VPS · Kubernetes · Web Hosting · Email · IP · vRack · Load Balancer · SMS
Installation · Authentication · API Reference · Error Handling
$ovh = new Ovh(new OvhClient($appKey, $appSecret, $consumerKey)); $ovh->domains()->addRecord('example.com', 'A', '203.0.113.10', 'www'); $ovh->cloud()->createInstance($projectId, ['name' => 'web01', 'flavorId' => '...', 'imageId' => '...']); $ovh->dedicatedServers()->reboot('ns3000000.ip-1-2-3.eu');
Framework-agnostic core — usable from any PHP project, script, or worker — with an optional bundle for first-class Symfony integration. The OVHcloud $1$ request signature is implemented natively (no dependency on the official ovh/ovh wrapper), with automatic clock-drift correction, typed exceptions, opt-in retries, and a comment-free, strictly typed codebase (PHP 8.2+, declare(strict_types=1) everywhere).
Table of Contents
- Features
- Requirements
- Installation
- Authentication
- Quick Start (plain PHP)
- Symfony Integration (optional)
- Architecture
- API Reference
- Responses
- Error Handling
- Testing
- Security Notes
- WHMCS module
- License
Features
| 🔐 Native request signing | The OVHcloud $1$ SHA-1 signature scheme implemented from scratch — no ovh/ovh dependency — with lazy clock-drift correction against /auth/time |
| 🧩 19 product modules | One facade covering the whole api.ovh.com/1.0 surface: domains, cloud, dedicated, VPS, Kubernetes, databases, hosting, email, IP, vRack, load balancers, CDN, licenses, orders, SMS, logs, support, VoIP |
| 🌍 All endpoints | ovh-eu, ovh-ca, ovh-us, kimsufi-eu/ca, soyoustart-eu/ca — pick yours or pass a custom base URL |
| 🎫 Credential flow built in | requestCredentials() mints a consumer key with the access rules you need and returns the validation URL |
| 🚨 Production-grade errors | 400/403/404/409/429 mapped to dedicated exceptions exposing OVHcloud's errorCode; opt-in automatic retries with backoff |
| 🔓 Nothing sealed off | The client's get/post/put/delete accept any path, and me()->fetch() / cloud()->fetch() reach any sub-resource, so 100% of the API is one call away |
| 🛠 Framework-agnostic | Only hard dependency is symfony/http-client; the optional Symfony bundle adds semantic config and autowiring |
| ✅ Fully unit-tested | 15 tests against MockHttpClient, including exact signature verification — no network required |
Requirements
| Dependency | Version |
|---|---|
| PHP | >= 8.2 |
| OVHcloud | an application key + secret, and a consumer key (create here) |
| Symfony | 6.4 LTS or 7.x — optional, only for the bundle integration |
Installation
If the package is published on Packagist:
composer require chuckbartowski/ovh-sdk
Authentication
OVHcloud signs every request with X-Ovh-Signature: $1$ + sha1(secret + "+" + consumerKey + "+" + method + "+" + url + "+" + body + "+" + timestamp). This SDK does all of it for you — you only provide three credentials:
- Application key + Application secret — identify your app (create an application).
- Consumer key — identifies the authorization granted to your app on an account.
The fastest path is api.ovh.com/createToken which gives you all three at once. To mint a consumer key programmatically with a chosen scope:
$client = new OvhClient($appKey, $appSecret, null, 'ovh-eu'); $creds = $client->requestCredentials( accessRules: [ ['method' => 'GET', 'path' => '/*'], ['method' => 'POST', 'path' => '/domain/*'], ['method' => 'DELETE', 'path' => '/domain/zone/*'], ], redirection: 'https://your-app.example.com/ovh/callback', ); $consumerKey = $creds->data('consumerKey'); // store this $validationUrl = $creds->data('validationUrl'); // user opens this once to approve
The clock-drift correction is automatic: the first signed call fetches OVHcloud's server time from /auth/time and offsets every subsequent timestamp, so a skewed local clock never breaks the signature.
Quick Start (plain PHP)
use ChuckBartowski\OvhSdk\Client\OvhClient; use ChuckBartowski\OvhSdk\Ovh; $ovh = new Ovh(new OvhClient( applicationKey: getenv('OVH_APP_KEY'), applicationSecret: getenv('OVH_APP_SECRET'), consumerKey: getenv('OVH_CONSUMER_KEY'), endpoint: 'ovh-eu', )); $account = $ovh->me()->info(); $ovh->domains()->addRecord('example.com', 'A', '203.0.113.10', 'www'); $ovh->domains()->refreshZone('example.com');
Client constructor signature:
new OvhClient( string $applicationKey, string $applicationSecret, ?string $consumerKey = null, // null → only requestCredentials() is allowed string $endpoint = 'ovh-eu', // or a full custom base URL float $timeout = 30.0, bool $retryFailed = false, // retry 429/5xx with exponential backoff int $maxRetries = 3, ?HttpClientInterface $httpClient = null, // inject your own (proxy, scoped, mock…) );
Symfony Integration (optional)
Register the bundle, then create config/packages/ovh_sdk.yaml:
ovh_sdk: application_key: '%env(OVH_APP_KEY)%' application_secret: '%env(OVH_APP_SECRET)%' consumer_key: '%env(OVH_CONSUMER_KEY)%' endpoint: 'ovh-eu' retry_failed: true
The Ovh facade is then autowirable in controllers, services, commands, and message handlers.
Architecture
src/
├── OvhSdkBundle.php Symfony bundle: config tree + service wiring (optional)
├── Ovh.php Facade: entry point for all modules
├── Client/
│ └── OvhClient.php Native $1$ signing, clock-drift, endpoints, retries, credential flow
├── Response/
│ └── ApiResponse.php Normalized response (+ items/first/as/asList helpers)
├── Exception/
│ ├── OvhSdkExceptionInterface.php ApiException.php
│ ├── ResourceNotFoundException.php ForbiddenException.php
│ ├── ConflictException.php InvalidRequestException.php
│ ├── RateLimitException.php AuthenticationException.php TransportException.php
└── Api/
├── AbstractApi.php MeApi.php DomainApi.php DedicatedServerApi.php
├── VpsApi.php CloudApi.php CloudDatabaseApi.php KubernetesApi.php
├── WebHostingApi.php EmailApi.php IpApi.php VrackApi.php
├── LoadBalancerApi.php CdnApi.php LicenseApi.php OrderApi.php
└── SmsApi.php DbaasLogsApi.php SupportApi.php VoipApi.php
API Reference
Every method returns an ApiResponse and throws on failure. Service names (domains, VPS, dedicated servers…) are URL-encoded for you.
Account (me)
$ovh->me() — info(), update(), bills() / bill(id), orders() / order(id), paymentMethods(), contacts(), SSH keys (sshKeys, addSshKey, deleteSshKey), API credentials (apiApplications, apiCredentials, revokeCredential), iamPolicies(), and fetch(path) for any other /me/* endpoint.
Domains & DNS
$ovh->domains() — domain lifecycle (list, find, serviceInfos, nameservers, updateNameservers, dnssec) and the full DNS zone editor:
| Method | Endpoint |
|---|---|
zones() / zone(name) |
/domain/zone |
records(zone, filters) / record(zone, id) |
/domain/zone/{zone}/record |
addRecord(zone, fieldType, target, subDomain, ttl) |
POST …/record |
updateRecord(zone, id, fields) / deleteRecord(zone, id) |
PUT/DELETE …/record/{id} |
refreshZone(zone) |
POST …/refresh — required to apply record changes |
exportZone(zone) |
BIND export |
$id = $ovh->domains()->addRecord('example.com', 'MX', '10 mail.example.com.', '')->data('id'); $ovh->domains()->refreshZone('example.com');
Dedicated servers
$ovh->dedicatedServers() — list, find, update, serviceInfos, hardware, network, reboot, reinstall(name, options) + installStatus, bootOptions, setMonitoring, ipmiAccess, interventions, tasks.
VPS
$ovh->vps() — list, find, update, power (reboot, start, stop), reinstall(name, options), images, ips, snapshots (snapshots, createSnapshot), disks, tasks.
Public Cloud
$ovh->cloud() — every method takes the $projectId first:
| Group | Methods |
|---|---|
| Projects | projects(), project(id), usageCurrent(id) |
| Instances | instances, instance, createInstance, deleteInstance, rebootInstance, startInstance, stopInstance, reinstallInstance, resizeInstance |
| Volumes | volumes, createVolume, deleteVolume, attachVolume, detachVolume, volumeSnapshots |
| Catalog | images, flavors, regions |
| Network | privateNetworks, createPrivateNetwork, publicNetworks |
| Access | sshKeys, addSshKey, users, createUser, storageContainers |
Anything else on a project: cloud()->fetch($projectId, '/…').
Managed Databases & Kubernetes
$ovh->cloudDatabases() — services(projectId), list(projectId, engine) (postgresql, mysql, redis, mongodb, kafka…), create, deleteCluster, databases, users, createUser, backups.
$ovh->kubernetes() — clusters, cluster, create, update, deleteCluster, kubeconfig, reset, node pools (nodePools, createNodePool, updateNodePool, deleteNodePool), nodes.
Web Hosting
$ovh->webHosting() — list, find, attached domains, databases (databases, createDatabase), cron, users, SSL (ssl, requestSsl), ovhConfigs, tasks.
$ovh->email() — MX Plan (domains, accounts, createAccount, deleteAccount, changePassword, redirections, mailing lists) and Exchange (exchangeServices, exchangeAccounts).
IP
$ovh->ips() — list, find, reverse DNS (reverse, setReverse, deleteReverse), move, park, firewall (firewall, enableFirewall, firewallRules, addFirewallRule), DDoS mitigation (mitigation, enableMitigation).
vRack
$ovh->vrack() — list, find, and membership management: dedicated servers, cloud projects, IP blocks (add/remove for each).
Load Balancer
$ovh->loadBalancers() — IPLB services: frontends/createFrontend, backends/createBackend, servers/addServer, pendingChanges, and refresh() to apply staged changes.
CDN, Licenses, Orders, SMS, Logs, Support, VoIP
$ovh->cdn()— dedicated & website CDN, domains,flushCache.$ovh->licenses()— Windows, cPanel, Plesk, DirectAdmin, Office licenses.$ovh->orders()— cart lifecycle (createCart,addItem,checkout) and publiccatalog.$ovh->sms()—send(service, receivers, message), jobs, outgoing/incoming, senders.$ovh->dbaasLogs()— Logs Data Platform: streams, dashboards, inputs, aliases.$ovh->support()— tickets (create,reply,close,messages).$ovh->voip()— Telephony: billing accounts, lines, numbers, consumption,click2Call.
Generic access
Any endpoint not wrapped by a module stays one call away — signing, endpoint and error handling still apply:
$ovh->client()->get('/dedicated/nasha'); $ovh->client()->post('/cloud/project/'.$id.'/ai/notebook', $payload);
Responses
$response = $ovh->domains()->records('example.com'); $response->success; // bool $response->statusCode; // int $response->data; // decoded JSON (OVHcloud often returns arrays of IDs) $response->items(); // list form, e.g. [12, 34, 56] $response->first(); // first item or null $response->errorCode; // OVHcloud error code on failure, e.g. 'NOT_FOUND' $response->as(Model::class); // hydrate a single object (bring your own model) $response->asList(Model::class); // hydrate a collection
Note the OVHcloud idiom: list endpoints usually return arrays of identifiers (record IDs, service names), which you then fetch individually — records() returns IDs, record($zone, $id) returns the detail.
Error Handling
All SDK exceptions implement OvhSdkExceptionInterface:
| Exception | Thrown when | Extras |
|---|---|---|
ApiException |
The API reported a failure (modules validate automatically) | getErrors(), getStatusCode(), getErrorCode(), getRaw() |
↳ InvalidRequestException |
HTTP 400 (bad parameters, invalid signature…) | |
↳ ForbiddenException |
HTTP 403 (consumer key lacks the access rule) | |
↳ ResourceNotFoundException |
HTTP 404 | |
↳ ConflictException |
HTTP 409 | |
↳ RateLimitException |
HTTP 429 | |
AuthenticationException |
Missing credentials, or HTTP 401 (invalid/expired consumer key) | thrown before any request when credentials are missing |
TransportException |
Network error, TLS failure, timeout, or invalid JSON | wraps the symfony/http-client exception |
try { $ovh->domains()->deleteRecord('example.com', $id); } catch (ForbiddenException) { // consumer key was not granted DELETE on /domain/zone/* }
Testing
The suite runs entirely offline against MockHttpClient — including a test that recomputes the $1$ signature byte-for-byte:
composer install vendor/bin/phpunit
Security Notes
- The application secret and consumer key are passed with
#[\SensitiveParameter], so they never appear in stack traces. - Scope consumer keys tightly with
accessRules— grantGET /domain/*, notALL /*, when a service only reads domains. - Keep credentials in
.env.localor your secret vault — never commit them. - Destructive calls (
deleteInstance,deleteRecord,deleteCluster, dedicatedreinstall) are irreversible — gate them behind confirmation flows.
WHMCS module
A ready-to-use WHMCS registrar module ships in whmcs/modules/registrars/ovhsdk/. It plugs OVHcloud domains into WHMCS's DNS and nameserver management through this SDK.
Install
composer require chuckbartowski/ovh-sdkin your WHMCS root.- Copy the
ovhsdkfolder into<whmcs>/modules/registrars/. - Activate OVHcloud (SDK) in System Settings » Domain Registrars and enter your application key / secret / consumer key and endpoint.
| WHMCS action | SDK call |
|---|---|
| Get / Save nameservers | domains()->records(... NS) / updateNameservers() |
| Get DNS records | domains()->records() + record() (MX priority split out) |
| Save DNS records | replace all records, then refreshZone() |
Domain registration and transfers go through OVHcloud's order cart (orders()), which is a multi-step purchase flow — the registrar hooks above cover the day-to-day DNS management that WHMCS drives automatically.
License
MIT