A modern, fully typed PHP SDK for the entire Scaleway API — 35 product modules, one elegant facade.
Instances · Elastic Metal · Kubernetes · Serverless · Databases · DNS · Load Balancers · Secret Manager · and everything in between
$scaleway = new Scaleway(new ScalewayClient(secretKey: getenv('SCW_SECRET_KEY')));
$server = $scaleway->instances()->createServer('web01', 'DEV1-S', 'ubuntu_jammy');
$scaleway->instances()->powerOn($server->data('server')['id']);
$scaleway->dns()->addRecord('example.com', 'www', 'A', '203.0.113.10');Framework-agnostic core — usable from any PHP project, script, or worker — with an optional bundle for first-class Symfony integration. Authenticated with IAM secret keys, zone/region aware, typed exceptions, automatic pagination, and a comment-free, strictly typed codebase (PHP 8.2+, declare(strict_types=1) everywhere).
- Features
- Requirements
- Installation
- Quick Start (plain PHP)
- Symfony Integration (optional)
- Architecture
- Zones, Regions and Projects
- API Reference
- Instances
- Elastic Metal
- Apple Silicon
- Block Storage
- VPC & IPAM
- Public Gateways
- Load Balancers
- Managed Databases (RDB)
- Managed Redis
- Kubernetes Kapsule
- Container Registry
- Serverless Containers & Functions
- Serverless Jobs
- Web Hosting
- Transactional Email
- Secret Manager
- DNS & Domains
- Account, IAM & Billing
- Instance Scaling Groups
- File Storage
- Elastic Metal Flexible IPs
- Edge Services
- Managed MongoDB
- Serverless SQL Databases
- Key Manager
- Managed Inference
- Messaging: NATS, Queues & Topics
- IoT Hub
- Cockpit
- Audit Trail
- Marketplace
- Coverage notes
- Responses
- Error Handling
- Testing
- Security Notes
- WHMCS module
- License
| 🧩 35 product modules | One facade covering the whole api.scaleway.com surface — every slug and version verified against the official API documentation |
| 🌍 Zone/region aware | Configure default_zone / default_region once; every method accepts a per-call override and the SDK builds the right /zones/… or /regions/… path |
| 🎯 Project injection | Set default_project_id once and creation calls fill project / project_id automatically (explicit values always win) |
| 📦 Smart responses | items() auto-detects Scaleway's per-product envelope keys, totalCount() reads body or X-Total-Count, paginate() streams all pages, and 272 generated typed models hydrate via as()/asList() |
| 🚨 Production-grade errors | 404/409/429/quota mapped to dedicated exceptions, opt-in automatic retries with backoff, and waiters for async resources |
| 🔓 Nothing sealed off | get/post/put/patch/delete accept any path, so an endpoint not wrapped by a module 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 | 62 tests against MockHttpClient — no network required |
| Dependency | Version |
|---|---|
| PHP | >= 8.2 |
| Scaleway | an IAM API key (secret key) |
| Symfony | 6.4 LTS or 7.x — optional, only for the bundle integration |
The package is published on Packagist:
composer require chuckbartowski/scaleway-sdkNo framework required — build the client and go:
use ChuckBartowski\ScalewaySdk\Client\ScalewayClient;
use ChuckBartowski\ScalewaySdk\Scaleway;
$scaleway = new Scaleway(new ScalewayClient(
secretKey: getenv('SCW_SECRET_KEY'),
defaultProjectId: getenv('SCW_DEFAULT_PROJECT_ID'),
defaultZone: 'fr-par-1',
defaultRegion: 'fr-par',
));
$server = $scaleway->instances()->createServer('web01', 'DEV1-S', 'ubuntu_jammy');
$scaleway->instances()->powerOn($server->data('server')['id']);
$scaleway->dns()->addRecord('example.com', 'www', 'A', '203.0.113.10');Client constructor signature:
new ScalewayClient(
string $secretKey, // IAM API key secret
string $defaultProjectId = '', // auto-injected into creation calls
string $defaultZone = 'fr-par-1',
string $defaultRegion = 'fr-par',
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…)
);Register the bundle:
// config/bundles.php
return [
ChuckBartowski\ScalewaySdk\ScalewaySdkBundle::class => ['all' => true],
];Then create config/packages/scaleway_sdk.yaml:
scaleway_sdk:
secret_key: '%env(SCW_SECRET_KEY)%'
default_project_id: '%env(SCW_DEFAULT_PROJECT_ID)%'
default_zone: 'fr-par-1'
default_region: 'fr-par'| Key | Type | Default | Description |
|---|---|---|---|
secret_key |
string | required | IAM API key secret, sent as X-Auth-Token |
default_project_id |
string | '' |
Project auto-injected into creation calls |
default_zone |
string | fr-par-1 |
Zone for zoned products (Instances, Elastic Metal, LB) |
default_region |
string | fr-par |
Region for regional products (RDB, K8s, VPC, Registry…) |
timeout |
float | 30.0 |
Per-request timeout in seconds |
retry_failed |
bool | false |
Retry transient failures (429, 5xx) with exponential backoff |
max_retries |
int | 3 |
Maximum retry attempts when retry_failed is enabled |
The Scaleway facade is then autowirable in controllers, services, commands, and message handlers. The bundle reuses your application's http_client service when available and falls back to a native client otherwise.
src/
├── ScalewaySdkBundle.php Symfony bundle: config tree + service wiring (optional)
├── Scaleway.php Facade: entry point for all modules
├── Client/
│ └── ScalewayClient.php Transport, X-Auth-Token, JSON handling, defaults
├── Response/
│ └── ApiResponse.php Immutable normalized response
├── Exception/
│ ├── ScalewaySdkExceptionInterface.php
│ ├── ApiException.php API answered but reported a failure
│ ├── AuthenticationException.php
│ └── TransportException.php Network / TLS / timeout / invalid JSON
└── Api/
├── AbstractApi.php zonal()/regional() path builders + project injection
├── InstanceApi.php BareMetalApi.php AppleSiliconApi.php
├── BlockStorageApi.php VpcApi.php PublicGatewayApi.php
├── IpamApi.php LoadBalancerApi.php RdbApi.php
├── RedisApi.php KubernetesApi.php RegistryApi.php
├── ContainerApi.php FunctionApi.php WebHostingApi.php
├── TransactionalEmailApi.php SecretManagerApi.php DnsApi.php
└── AccountApi.php IamApi.php BillingApi.php
Design decisions:
- Facade + lazy modules:
Scalewayinstantiates each module on first use and caches it. - Modules always validate: every module method calls
ensureSuccess()internally and throwsApiExceptionon failure. To inspect a failed response without an exception, drop down to the client level. - Per-product versioning is encapsulated: each module knows its product's API version (
instance/v1,vpc/v2,domain/v2beta1…) so callers never touch versioned paths.
Scaleway splits its catalog between zoned products (Instances, Elastic Metal, Load Balancers — fr-par-1, fr-par-2, nl-ams-1, pl-waw-1…) and regional products (RDB, Kubernetes, VPC, Registry, IPAM — fr-par, nl-ams, pl-waw). The SDK handles both:
$scaleway->instances()->servers(); // default zone (fr-par-1)
$scaleway->instances()->servers(zone: 'nl-ams-1'); // per-call override
$scaleway->databases()->instances(region: 'pl-waw'); // regional overrideMost resources belong to a Project. Set default_project_id once and every creation call injects it (project for Instances, project_id for the other products); passing it explicitly in options always takes precedence.
Every method returns an ApiResponse and throws on failure (see Error Handling). Zoned methods accept a trailing ?string $zone = null, regional ones ?string $region = null.
$scaleway->instances() — the full instance/v1 surface.
| Group | Methods |
|---|---|
| Servers | servers(), server(id), createServer(name, commercialType, image, options), updateServer(id, fields), deleteServer(id) |
| Power | powerOn, powerOff, reboot, terminate, backup(id, ?name) — or raw serverAction(id, action, options) |
| Volumes | volumes(), volume(id), createVolume(name, type, sizeBytes), updateVolume, deleteVolume |
| Snapshots | snapshots(), createSnapshot(volumeId, name), deleteSnapshot(id) |
| Images | images(), createImage(name, rootVolumeId), deleteImage(id) |
| Flexible IPs | ips(), createIp(), attachIp(ipId, serverId), detachIp(ipId), deleteIp(id) |
| Security groups | securityGroups(), createSecurityGroup(name), securityGroupRules(id), createSecurityGroupRule(id, rule) |
| Private NICs | privateNics(serverId), createPrivateNic(serverId, privateNetworkId), deletePrivateNic(...) |
Power actions return a task — poll server(id) until the state settles. Volume sizes are in bytes (50_000_000_000 for 50 GB).
$scaleway->bareMetal() — baremetal/v1: servers(), createServer(offerId, name), install(id, osId, hostname, sshKeyIds), start/stop/reboot(id, bootType), metrics(id), offers(), oses(), updateIp(serverId, ipId, fields) (reverse DNS).
$scaleway->appleSilicon() — apple-silicon/v1alpha1 (zoned, fr-par-3): servers(), createServer(type, ?name) (types: M1-M, M2-M, M2-L…), deleteServer, rebootServer, reinstallServer, serverTypes(). Mind the 24-hour minimum allocation period enforced by Apple's licensing.
$scaleway->blockStorage() — block/v1alpha1 (SBS, the low-latency block storage that replaces Instance b_ssd volumes): volumes(), createVolume(name, sizeBytes, ?perfIops) (5000 or 15000 IOPS), createVolumeFromSnapshot(name, snapshotId), updateVolume (resize, IOPS change), deleteVolume, snapshots(), createSnapshot(volumeId, name), deleteSnapshot.
$scaleway->vpc() — vpc/v2: vpcs(), createVpc(name), privateNetworks(), createPrivateNetwork(name), updatePrivateNetwork, deletePrivateNetwork.
$scaleway->ipam() — ipam/v1: ips(query) (filter by resource, private network…), bookIp(source, options), updateIp, releaseIp.
$scaleway->publicGateways() — vpc-gw/v2 (zoned): gateways(), createGateway(type) (VPC-GW-S…), updateGateway, deleteGateway(id, deleteIp), attachNetwork(gatewayId, privateNetworkId) / detachNetwork(gatewayNetworkId), and NAT: patRules(query), createPatRule(gatewayId, publicPort, privateIp, privatePort, protocol), deletePatRule.
$scaleway->publicGateways()->createPatRule('gw-uuid', 2222, '192.168.1.10', 22, 'tcp');$scaleway->loadBalancers() — lb/v1 (zoned): loadBalancers(), createLoadBalancer(name), deleteLoadBalancer(id, releaseIp), backends (createBackend(lbId, name, protocol, port), setBackendServers(backendId, ips)), frontends (createFrontend(lbId, name, inboundPort, backendId)), certificates (createCertificate(lbId, name, options) — Let's Encrypt or custom).
$scaleway->databases() — rdb/v1: instances(), createInstance(name, engine, nodeType, userName, password) (engine e.g. PostgreSQL-15), databases(instanceId), createDatabase, users(instanceId), createUser(instanceId, name, password, admin), setPrivilege(instanceId, db, user, permission) (readonly, readwrite, all, custom, none), backups(), createBackup, restoreBackup.
$scaleway->redis() — redis/v1 (zoned): clusters(), createCluster(name, version, nodeType, userName, password) (node types RED1-MICRO…), updateCluster, migrateCluster(id, fields) (version or node-type migration), deleteCluster, ACLs (addAclRules(clusterId, rules), deleteAclRule(aclId)).
$scaleway->kubernetes() — k8s/v1: clusters(), createCluster(name, version, cni, pools), upgradeCluster(id, version), deleteCluster(id, withAdditionalResources), kubeconfig(clusterId), pools (createPool(clusterId, name, nodeType, size), updatePool for autoscaling), nodes (nodes(clusterId), replaceNode, rebootNode), versions().
$scaleway->registry() — registry/v1: namespaces(), createNamespace(name, isPublic), deleteNamespace, images(), deleteImage, tags(imageId), deleteTag.
$scaleway->containers() — containers/v1beta1 (regional): namespaces(), createNamespace(name), containers(), createContainer(namespaceId, name, registryImage), updateContainer, deployContainer(id), deleteContainer, custom domains() / createDomain(containerId, hostname).
$scaleway->functions() — functions/v1beta1 (regional): same shape with createFunction(namespaceId, name, runtime), uploadUrl(functionId, contentLength) (presigned zip upload), deployFunction(id), runtimes().
The create → upload/configure → deploy cycle matters: configuration changes only go live after an explicit deploy.
$scaleway->webHosting() — webhosting/v1 (regional, Scaleway's managed cPanel offering): hostings(), createHosting(offerId, domain, email), updateHosting, deleteHosting, offers(), controlPanels().
$scaleway->transactionalEmail() — tem/v1alpha1 (regional): sender domains (domains(), createDomain(domainName), checkDomain(id) for SPF/DKIM validation, revokeDomain) and sending (sendEmail(fromEmail, to, subject, text, html), emails(query), email(id), cancelEmail(id)).
$scaleway->transactionalEmail()->sendEmail('noreply@example.com', ['user@example.com'], 'Welcome', 'Hello!');$scaleway->secrets() — secret-manager/v1beta1 (regional): secrets(), createSecret(name), deleteSecret, versions (versions(secretId), createVersion(secretId, plaintext) — base64 encoding handled for you, accessVersion(secretId, revision) defaulting to latest, enableVersion / disableVersion).
$scaleway->dns() — domain/v2beta1 (global, the zone is the domain name). Registrar side: domains(query) and domain(name) list the domain names attached to the account. Zone side:
| Method | Notes |
|---|---|
zones(query) / createZone(domain, subdomain) / deleteZone(zone) |
|
records(zone, query) |
filter with ['type' => 'A', 'name' => 'www'] |
addRecord(zone, name, type, data, ttl) |
wraps the changes payload for you |
deleteRecord(zone, name, type, ?data) |
omit data to delete all records of that name/type |
updateRecords(zone, changes) |
raw access to the full changes API (add/set/delete/clear) |
exportZone(zone) / refreshZone(zone) |
BIND export / force refresh |
$scaleway->account() — account/v3: projects(), createProject(name), updateProject, deleteProject.
$scaleway->iam() — iam/v1alpha1: apiKeys(), createApiKey(), deleteApiKey(accessKey), sshKeys(), createSshKey(name, publicKey), users(), applications(), createApplication(name), policies(), createPolicy(name, rules).
$scaleway->billing() — billing/v2beta1: consumptions(), invoices(), downloadInvoice(id), discounts().
$scaleway->jobs() — serverless-jobs/v1alpha2 (regional): job definitions CRUD (createDefinition(name, imageUri) with cron, resources and env in options), run(definitionId), runs() / runDetails(runId), stopRun(runId).
$definition = $scaleway->jobs()->createDefinition('nightly-report', 'rg.fr-par.scw.cloud/ns/report:latest');
$scaleway->jobs()->run($definition->data('id'));$scaleway->autoscaling() — autoscaling/v1alpha1 (zonal): instanceGroups() CRUD with capacity (min/max size, cooldown), templates() (createTemplate(name, commercialType) with volumes and tags), policies() (createPolicy(groupId, name, action, type) — scale up/down on metric or schedule), instanceGroupEvents(id) for the scaling history.
$scaleway->fileStorage() — file/v1alpha1 (regional): shared NFS-style filesystems attachable to Instances through Private Networks — filesystems() CRUD (createFilesystem(name, sizeBytes)), attachments(), filesystemTypes().
$scaleway->flexibleIps() — flexible-ip/v1alpha1 (zonal): failover IPs for Elastic Metal — ips() CRUD, attachToServer(ipId, serverId) / detachFromServer(ipId), virtual MACs (generateMac(ipId, macType), deleteMac) for virtualization setups.
$scaleway->edgeServices() — edge-services/v1beta1 (global): the CDN/WAF layer in front of Object Storage buckets and Load Balancers — pipelines() CRUD, per-pipeline stages (createBackendStage, createCacheStage, createTlsStage, createDnsStage), plans(), and cache purging (createPurgeRequest(pipelineId, ?assets) — omit assets to purge everything).
$scaleway->mongodb() — mongodb/v1 (regional): instances() CRUD (createInstance(name, version, nodeType, nodeNumber, userName, password, volume)), upgradeInstance, TLS certificate(instanceId), snapshots (createSnapshot, restoreSnapshot), users() management, databases(), public/private createEndpoint / deleteEndpoint, plus the nodeTypes() and versions() catalogs.
$scaleway->serverlessSql() — serverless-sqldb/v1alpha1 (regional): autoscaling PostgreSQL billed to the query — databases() CRUD with cpu_min/cpu_max bounds, restoreDatabase(id, backupId), backups(databaseId), exportBackup.
$scaleway->keyManager() — key-manager/v1alpha1 (regional): Scaleway's KMS — keys() CRUD, encrypt(keyId, plaintext) / decrypt(keyId, ciphertext) (base64 handled for you), envelope encryption via generateDataKey, rotateKey, asymmetric sign/verify, publicKey(id), enableKey/disableKey.
$encrypted = $scaleway->keyManager()->encrypt('key-uuid', 'sensitive payload');$scaleway->inference() — inference/v1 (regional): dedicated LLM deployments — deployments() CRUD (createDeployment(name, modelId, nodeTypeName, endpoints)), models() catalog + importModel(source) from Hugging Face or object storage, nodeTypes() (GPU offers), endpoint management with public or Private Network exposure.
$scaleway->messaging() — mnq/v1beta1 (regional), the management plane for the three messaging products: NATS (natsAccounts(), createNatsAccount, natsCredentials, createNatsCredentials), Queues/SQS (activateQueues(), queuesInfo(), createQueuesCredentials(name, permissions), deactivateQueues()), Topics/SNS (activateTopics(), topicsInfo(), createTopicsCredentials). The data planes speak native NATS/SQS/SNS protocols — use the matching client libraries with these credentials.
$scaleway->iot() — iot/v1 (regional): MQTT hubs (hubs() CRUD, enableHub/disableHub, hubMetrics), devices (createDevice(hubId, name), certificates via deviceCertificate / renewDeviceCertificate, enable/disable), message routes() (to databases, S3, functions…) and external networks().
$scaleway->cockpit() — cockpit/v1 (global): the observability stack — Grafana users (grafanaUsers(), createGrafanaUser(login, role), deleteGrafanaUser), prebuilt productDashboards(), and syncDatasources() to refresh data sources after enabling new products.
$scaleway->auditTrail() — audit-trail/v1alpha1 (regional): who did what, when — events() (resource events, filterable by product/verb/date), authenticationEvents(), systemEvents(), products() (which products are integrated), and export jobs to Object Storage (createExport, deleteExport).
$scaleway->marketplace() — marketplace/v2 (global, no authentication required): the public image catalog — images(), image(id), imageVersions(imageId), localImages() (per-zone IDs to feed instances()->createServer()), categories().
- The Instances module also exposes the catalog (
serverTypes(),serverTypesAvailability()) andplacementGroups()/createPlacementGroup(name, policyMode, policyType); Transactional Email includes webhooks, blocklists and project settings; Web Hosting includesbackups()/restoreBackup(). - Two path quirks verified against the official docs: Block Storage uses
block/v1with a singular/zone/{zone}/segment, and Transactional Email's slug istransactional-email(nottem). - Intentionally out of scope: Object Storage (S3 protocol — use any S3 client), the data planes of NATS/Queues/Topics (native NATS/SQS/SNS protocols — only their management API is wrapped), Dedibox, and beta data products (Kafka, Data Warehouse, Data Lab, OpenSearch, RabbitMQ). Any
api.scaleway.comendpoint remains reachable through the client's genericget/post/patch/deletemethods. - Products still in
alpha/betaat Scaleway may change paths on GA — each module encapsulates its version so an SDK update is a one-line change.
All calls return an immutable ApiResponse. Scaleway wraps every list under a product-specific key — here is what actually comes back from GET /instance/v1/zones/fr-par-1/servers:
{
"servers": [
{
"id": "d67f36bc-…",
"name": "web01",
"commercial_type": "DEV1-S",
"state": "running",
"public_ip": { "address": "203.0.113.10" },
"zone": "fr-par-1"
}
],
"total_count": 1
}The response object gives you three levels of access — raw, keyed, and list-aware:
$response = $scaleway->instances()->servers();
$response->success; // bool
$response->statusCode; // int
$response->data; // the decoded JSON body above (null on 204)
$response->data('servers'); // keyed access with optional default
$response->errors; // list<string>
$response->raw; // complete decoded payload
$response->headers; // response headers (lowercased keys)
$response->items(); // the wrapped list, envelope key auto-detected
$response->items('servers'); // or with an explicit key
$response->first(); // first item, null when the list is empty
$response->totalCount(); // body total_count, falls back to X-Total-Count header
$response->header('x-total-count');items() works across every module without configuration: it finds the single list key in the payload (servers, clusters, secrets, records…), so switching products never changes your consuming code.
Every response object documented in Scaleway's official OpenAPI schemas has a matching PHP class — 272 readonly models across all 35 products, generated from the schemas themselves (tools/generate-models.py), so field names and types match the API exactly. Hydrate them straight from any response:
use ChuckBartowski\ScalewaySdk\Model\Instance\Server;
use ChuckBartowski\ScalewaySdk\Model\Rdb\Instance;
use ChuckBartowski\ScalewaySdk\Model\Domain\Record;
$server = $scaleway->instances()->server($id)->as(Server::class);
$server->name; // ?string — snake_case fields become camelCase
$server->commercialType; // ?string 'DEV1-S'
$server->publicIp; // ?array ['address' => '203.0.113.10', …]
$server->protected; // ?bool
$server->raw; // the complete original payload, nothing lost
$db = $scaleway->databases()->instance($id)->as(Instance::class);
$db->engine; // 'PostgreSQL-15'
$db->isHaCluster; // ?bool
foreach ($scaleway->dns()->records('example.com')->asList(Record::class) as $record) {
echo "{$record->name} {$record->type} {$record->data} (ttl {$record->ttl})", PHP_EOL;
}as(Model::class)hydrates a single resource — it unwraps single-key envelopes ({"server": {…}}) and handles bare objects (RDB, MongoDB…) automatically; pass an explicit key as second argument when needed.asList(Model::class)hydrates a wrapped collection, reusing the same envelope auto-detection asitems().- Hydration is lossless and lenient: unknown or new API fields stay available in
->raw, and malformed values degrade tonull/[]instead of throwing. - Models are plain data classes — using them is always optional, the array accessors above keep working.
List endpoints are paginated (page + page_size, or per_page for the Instance API; 100 items max per page). The client can walk all pages for you and stream items through a generator — memory-friendly even on large fleets:
foreach ($scaleway->client()->paginate('/instance/v1/zones/fr-par-1/servers', itemsKey: 'servers', perPageParam: 'per_page') as $server) {
echo $server['name'], PHP_EOL;
}
foreach ($scaleway->client()->paginate('/k8s/v1/regions/fr-par/clusters') as $cluster) {
// regional products use the default page_size parameter, envelope key auto-detected
}All SDK exceptions implement ScalewaySdkExceptionInterface, so a single catch covers everything:
| Exception | Thrown when | Extras |
|---|---|---|
ApiException |
The API answered but reported a failure (module methods validate automatically) | getErrors(), getStatusCode(), getRaw() |
↳ ResourceNotFoundException |
HTTP 404 — the resource does not exist (anymore) | |
↳ ConflictException |
HTTP 409 — the resource is in a conflicting state (still attached, still in use…) | |
↳ RateLimitException |
HTTP 429 — you are being throttled | getRetryAfter(): ?int (seconds, from the Retry-After header) |
↳ QuotaExceededException |
The API reports the quotas_exceeded error type |
|
AuthenticationException |
The secret key is missing or the API answered HTTP 401 | thrown before any request when the key is empty |
TransportException |
Network error, TLS failure, timeout, or a non-JSON response body | wraps the underlying symfony/http-client exception |
The four subclasses extend ApiException, so existing catch (ApiException $e) blocks keep working — narrow only where the distinction matters:
try {
$scaleway->instances()->server($id);
} catch (ResourceNotFoundException) {
$this->markAsDeleted($id);
} catch (RateLimitException $e) {
$this->requeue($id, delaySeconds: $e->getRetryAfter() ?? 30);
}Transient failures (429, 5xx, network hiccups) can be retried automatically with exponential backoff, powered by Symfony's RetryableHttpClient:
$client = new ScalewayClient('key', retryFailed: true, maxRetries: 3);scaleway_sdk:
retry_failed: true
max_retries: 3Most Scaleway resources provision asynchronously — the create call returns immediately while the resource is still provisioning/starting. Waiters poll until the resource reaches a stable state, fail fast when it enters an error state, and throw on timeout:
$scaleway->instances()->powerOn($id);
$scaleway->instances()->waitForServerState($id, 'running');
$db = $scaleway->databases()->createInstance('main-db', 'PostgreSQL-15', 'db-dev-s', 'admin', $pwd);
$scaleway->databases()->waitForInstanceReady($db->data('id'), timeout: 900.0);| Waiter | Success state | Fails fast on | Defaults |
|---|---|---|---|
instances()->waitForServerState($id, $states) |
the state(s) you pass (running, stopped…) |
locked |
300 s, poll 2 s |
databases()->waitForInstanceReady($id) |
ready |
error, disk_full |
900 s, poll 5 s |
kubernetes()->waitForClusterReady($id) |
ready |
error |
900 s, poll 5 s |
bareMetal()->waitForServerReady($id) |
ready |
error, unknown |
3600 s, poll 10 s |
loadBalancers()->waitForReady($id) |
ready |
error |
600 s, poll 3 s |
Every waiter returns the final ApiResponse (so you can chain ->as(Model::class)) and accepts timeout, interval, and the usual zone/region override.
Validation failures are readable out of the box — message, type, and each details entry are flattened into getErrors():
invalid_arguments: invalid argument(s)
commercial_type: unknown commercial type
To inspect a failed response without exceptions, use the client directly — client-level methods return the response as-is.
The suite runs entirely offline against MockHttpClient:
composer install
vendor/bin/phpunitTo test your own services, inject a ScalewayClient built with a mock:
use ChuckBartowski\ScalewaySdk\Client\ScalewayClient;
use ChuckBartowski\ScalewaySdk\Scaleway;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\JsonMockResponse;
$http = new MockHttpClient(new JsonMockResponse(['servers' => []]));
$scaleway = new Scaleway(new ScalewayClient('key', httpClient: $http));- The secret key is passed with
#[\SensitiveParameter], so it never appears in stack traces. - Scope IAM API keys with policies (
createPolicy()) instead of using an Owner key everywhere — one application per service, least privilege per policy rule. - Keep credentials in
.env.localor your secret vault — never commit them. - Object Storage is intentionally out of scope: it speaks the S3 protocol on separate endpoints — use any S3 client with your Scaleway credentials.
terminateon an Instance deletes the server and its local volumes;deleteCluster(withAdditionalResources: true)deletes load balancers and volumes created by the cluster — gate destructive calls behind confirmation flows in your application.
A ready-to-use WHMCS provisioning module ships in whmcs/modules/servers/scalewaysdk/. It provisions a Scaleway Instance through this SDK — create + power on + wait for running, suspend (power off), unsuspend, terminate.
Install
composer require chuckbartowski/scaleway-sdkin your WHMCS root.- Copy the
scalewaysdkfolder into<whmcs>/modules/servers/. - Add a server with Type: Scaleway Instance (SDK) and your IAM secret key in the Access Hash field.
- Set the config options: Commercial type (
DEV1-S…), Image (ubuntu_jammy…), Zone (fr-par-1…), Project id.
The instance is named whmcs-<serviceid>, so the module locates it again on suspend/terminate without extra storage. Power actions use instances()->waitForServerState() to block until the instance settles.
MIT