ashio / openprovider-php-sdk
Typed PHP SDK for the whole Openprovider REST API (/v1), generated from the official specification
Requires
- php: >=8.2
- ext-json: *
- guzzlehttp/guzzle: ^7.15.2
- guzzlehttp/psr7: ^2.7
- psr/http-client: ^1.0.3
- psr/http-factory: ^1.1
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.75
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- psr/simple-cache: ^3.0
- vimeo/psalm: ^6.0
Suggests
- psr/simple-cache: Share the API token between processes with Psr16TokenStore
Provides
None
Conflicts
None
Replaces
None
README
Openprovider PHP SDK
The whole Openprovider REST API, typed, in PHP: generated from the official specification and held to it by tests.
Quick start · Guide · API reference · How it is built · Quality gates · Contributing · Changelog
Openprovider closes its /v1beta API to everyone on 30 June 2027. This SDK talks to /v1, its
successor, and covers every operation of the current reference. You call it with PHP types: models
for requests and responses, named arguments for filters, and one exception class per kind of
failure.
Nothing in it is written from memory. The services and models are generated from a pinned copy of Openprovider's own specification. A conformance audit runs every method against that specification on every pull request. The badges, the counts and the API reference on this page are generated from the code, and every example on it runs in the test suite.
use Ashio\OpenProvider\Pagination\Paginator; foreach (Paginator::all($openProvider->domain()->listDomains(...), ['status' => 'ACT']) as $domain) { echo $domain->domain?->name, '.', $domain->domain?->extension, ' expires ', $domain->expirationDate, PHP_EOL; }
At a glance
| Operations | 140 of the 140 in the current API reference, in 11 services |
| Models | 306 immutable, typed models and 4 sets of enum values |
| Error codes | 608, each mapped to an exception class |
| Paged listings | 24, 4 of them with a documented maximum page size |
| Experimental | 5 operations documented only by the developer portal |
| Spec | openprovider/api-documentation@e0bcf22e81 and the developer portal, retrieved 2026-11-09 |
| Tested on | PHP 8.2, 8.3, 8.4, 8.5, each with the lowest and the highest dependencies |
Why this SDK
- Complete, and proven complete. Every operation in the API reference has exactly one method, and no method exists outside it. A test fails the build otherwise.
- Correct on the wire. Each parameter goes where the specification says: the query or the
body, whatever the HTTP verb. Dotted names (
domain.name) are sent as they are, lists repeat their key, andfalse,0and''are sent rather than dropped. Each operation has its own HTTP contract test. - Typed all the way down. Responses arrive as immutable models with typed properties and
nested models. Fields the API adds later are kept in
$extrainstead of being lost or breaking anything. - Errors you can act on. Openprovider answers HTTP 500 to a failed login. The SDK ignores the status and classifies each of the API's documented error codes into an exception you can catch.
- Safe by default. Nothing is sent until you call an operation. Writes are never retried. An expired token triggers one new login and one repeat. Passwords and tokens never show up in logs, dumps or exception traces.
- Standards, not lock-in. Any PSR-18 HTTP client and PSR-17 factories (Guzzle by default), any PSR-16 cache for sharing the token between processes, and no global state.
Contents
- Requirements
- Installation
- Quick start
- Guide
- API reference
- How it is built
- Quality gates
- Development
- Security
- Versioning and support
- License
Requirements
- PHP 8.2 or newer, with
ext-json - A PSR-18 HTTP client. Guzzle 7 is installed with the SDK and used unless you pass another.
- An Openprovider account. Use the sandbox while you develop.
Installation
composer require ashio/openprovider-php-sdk
Quick start
Create the client once. No request is sent at this point: the SDK logs in when the first operation needs a token.
<?php require __DIR__ . '/vendor/autoload.php'; use Ashio\OpenProvider\Auth\Credentials; use Ashio\OpenProvider\Client\ApiClient; use Ashio\OpenProvider\Client\ClientConfig; use Ashio\OpenProvider\OpenProvider; $openProvider = new OpenProvider(ApiClient::create( ClientConfig::sandbox(), // ClientConfig::production() for the live API new Credentials( getenv('OPENPROVIDER_USERNAME') ?: throw new RuntimeException('Set OPENPROVIDER_USERNAME'), getenv('OPENPROVIDER_PASSWORD') ?: throw new RuntimeException('Set OPENPROVIDER_PASSWORD'), ), ));
Then call any operation. Each service is one accessor on $openProvider, and each operation is one
method:
use Ashio\OpenProvider\Model\Domain\DomainCheckDomainRequest; use Ashio\OpenProvider\Model\Domain\DomainDomain; $check = $openProvider->domain()->checkDomain(new DomainCheckDomainRequest( domains: [new DomainDomain(extension: 'com', name: 'example')], )); foreach ($check->data->results ?? [] as $result) { echo $result->domain, ': ', $result->status, PHP_EOL; // example.com: active }
Tip
Every PHP example on this page is executed by the test suite against responses built from the
specification (tests/Readme). An example that stops working fails the build.
Guide
Calling an operation
The method names and their arguments follow fixed rules, so you can read them straight off the API reference:
| In the specification | In PHP |
|---|---|
The tag group (DNS, Reseller/Customer) |
the service accessor: dns(), resellerCustomer() |
The operationId (ListZoneRecords, DeleteDomainMixin8) |
the method, in camelCase and without a Mixin<n> suffix: listZoneRecords(), deleteDomain() |
| Path parameters | the first arguments, in path order |
| The request body | the next argument: a model, such as ZoneCreateZoneRequest |
| Query parameters | optional named arguments, in camelCase: domain.name → $domainName, record_name_pattern → $recordNamePattern |
$records = $openProvider->dns()->listZoneRecords('example.com', limit: 50, recordNamePattern: 'www*', type: 'CNAME');
Named arguments are checked by PHP itself. A misspelled filter is an error before anything is sent; it is never silently ignored by the API.
Responses and models
Every method returns an ApiResponse. A call that returns at all has succeeded: any code other
than 0 is thrown as an exception (see Handling errors).
$response = $openProvider->resellerCustomer()->getReseller(); $reseller = $response->data; // ResellerReseller: a typed, immutable model echo $reseller->companyName, ' has ', $reseller->balance, ' available', PHP_EOL; foreach ($response->warnings as $warning) { // the call worked, but the API has a remark error_log(sprintf('Openprovider warning %d: %s', $warning->code, $warning->desc)); }
Each model has fromArray() and toArray(). A model never refuses data it does not know: unknown
keys and values of an unexpected type are kept in $extra, so nothing the API sends is lost, and
toArray() gives back exactly what was received.
use Ashio\OpenProvider\Model\Dns\ZoneZone; $zone = ZoneZone::fromArray(['name' => 'example.com', 'brand_new_field' => true]); echo $zone->name, PHP_EOL; // example.com print_r($zone->extra); // ['brand_new_field' => true] print_r($zone->toArray()); // ['name' => 'example.com', 'brand_new_field' => true]
Values from a fixed list stay plain strings, so a value Openprovider adds later cannot break your code. The known values are provided as constants:
use Ashio\OpenProvider\Model\ResellerCustomer\AttributesIndustry; $industry = AttributesIndustry::WEB_HOSTING; $known = in_array($industry, AttributesIndustry::VALUES, true); // true
Sending data
Request bodies are models with named, typed constructor arguments. Absent fields are left out of
the JSON. 0, false and '' are sent as they are.
use Ashio\OpenProvider\Model\Dns\ZoneCreateZoneRequest; use Ashio\OpenProvider\Model\Dns\ZoneDomain; use Ashio\OpenProvider\Model\Dns\ZoneRecord; $openProvider->dns()->createZone(new ZoneCreateZoneRequest( domain: new ZoneDomain(extension: 'com', name: 'example'), type: 'master', records: [ new ZoneRecord(name: '', type: 'A', value: '192.0.2.10', ttl: 900), new ZoneRecord(name: 'www', type: 'CNAME', value: 'example.com', ttl: 900), ], ));
Paging through listings
Paginator::all() takes a list method as a first-class callable, so you write listDomains(...)
with three literal dots. It then fetches pages as your loop asks for them. Each page uses the
endpoint's documented maximum, which the API reference shows as paged, max N,
or the API's default of 100 where no maximum is documented. Fewer, larger pages keep long listings
clear of the rate limits.
use Ashio\OpenProvider\Pagination\Paginator; $expiring = []; foreach (Paginator::all($openProvider->domain()->listDomains(...), ['status' => 'ACT']) as $domain) { if ($domain->expirationDate !== null && $domain->expirationDate < '2027-01-01') { $expiring[] = $domain->domain?->name . '.' . $domain->domain?->extension; } }
The second argument carries the method's other arguments, which are sent with every page.
pageSize: lowers the page size, but never raises it above the endpoint's maximum. A method that is
not a paged listing is refused before anything is sent.
Handling errors
Every exception the SDK throws implements OpenProviderException. API errors are classified by
the API's own error code, never by the HTTP status:
| Exception | Thrown when | API codes |
|---|---|---|
AuthenticationException |
The login was refused or the token is not accepted (after one automatic re-login on code 196). | 5 |
RateLimitedException |
A rate limit or a temporary block. Never retried automatically. | 2 |
MaintenanceException |
The API is in maintenance. Reads may be retried if you opt in. | 5 |
NotFoundException |
The object does not exist. | 13 |
ValidationException |
The API refused the input. | 226 |
ApiException |
Any other API error; the base class of the five above. | 357 |
TransportException |
The HTTP request itself failed (network, DNS, TLS, timeout). | — |
UnexpectedResponseException |
The API answered with something that is not a JSON envelope, or data of another type than the spec declares. | — |
InvalidArgumentException |
An argument the spec does not allow, caught before anything is sent. | — |
use Ashio\OpenProvider\Exception\ErrorCatalog; use Ashio\OpenProvider\Exception\NotFoundException; use Ashio\OpenProvider\Exception\OpenProviderException; use Ashio\OpenProvider\Exception\ValidationException; try { $zone = $openProvider->dns()->getZone('example.com')->data; } catch (NotFoundException) { $zone = null; } catch (ValidationException $e) { // The API's code and message, plus the documented fix when there is one. error_log(sprintf('%d %s. %s', $e->apiCode, $e->desc, ErrorCatalog::resolution($e->apiCode) ?? '')); throw $e; } catch (OpenProviderException $e) { throw $e; // any other API error, a network failure or an unexpected answer }
An ApiException carries everything from the envelope: apiCode, httpStatus, desc, data,
warnings and maintenance. ErrorCatalog::describe() and ErrorCatalog::resolution() give the
documented text for any code.
Authentication and tokens
- Lazy login. The first operation that needs a token logs in. Operations the API serves without a token, such as the login itself and the reseller signup, are sent without one.
- One token, reused. The token is kept and reused until shortly before it expires, then replaced. By default it lives in memory, for the life of the client.
- Expiry handled once. On code 196 (token expired or invalid), the client logs in again and repeats the call once. This also applies to writes, because the API refused the call before running it. Any other failure of a write is never repeated.
To share the token between processes or requests (for example PHP-FPM workers), give the client any PSR-16 cache:
use Ashio\OpenProvider\Auth\Psr16TokenStore; use Ashio\OpenProvider\Client\ApiClient; use Ashio\OpenProvider\Client\ClientConfig; $client = ApiClient::create(ClientConfig::production(), $credentials, tokenStore: new Psr16TokenStore($cache));
Tokens are stored per environment and username, and are masked wherever they could be printed.
Configuration
ClientConfig holds the connection settings and no credentials, so it is safe to log.
| Option | Default | |
|---|---|---|
environment |
(required) | Environment::Production (api.openprovider.eu) or Environment::Sandbox (api.sandbox.openprovider.nl). An unknown name throws instead of falling back to production. |
basePath |
/v1 |
/v1beta still works until 30 June 2027. |
timeout |
60.0 |
Seconds for a whole request. |
connectTimeout |
10.0 |
Seconds to connect. |
userAgent |
ashio-openprovider-php/<version> |
One line, never empty. |
ip |
none | Sent with the login, if your account restricts logins by IP. |
getRetry |
no retries | See Retries. |
use Ashio\OpenProvider\Client\ClientConfig; use Ashio\OpenProvider\Client\Environment; use Ashio\OpenProvider\Client\RetryPolicy; $config = new ClientConfig( Environment::from(getenv('OPENPROVIDER_ENV') ?: 'sandbox'), timeout: 30.0, connectTimeout: 5.0, getRetry: new RetryPolicy(maxRetries: 2), );
Retries
Retries are off by default, and only reads (GET) can be retried. A read is retried after a
network failure, an HTTP 5xx answer that is not JSON, or a maintenance code. It waits 250 ms,
1 s and 4 s before the first, second and third retry, then 4 s for any further one; pass
delaysMs: to change that.
Writes are sent exactly once. The API documents no idempotency, and it blocks identical writes for minutes, so a blind retry could register a domain twice or lock you out. Rate limits and blocks are never retried either. Back off and try later.
Using your own HTTP client
Any PSR-18 client works, with PSR-17 factories if you do not use Guzzle's:
use Ashio\OpenProvider\Client\ApiClient; $client = ApiClient::create($config, $credentials, httpClient: $httpClient);
The SDK never discovers implementations on its own, and it keeps no global state. Two clients with different accounts or environments can run side by side.
Dates
The API sends dates as strings without an offset, in the Europe/Amsterdam time zone. Models
keep them exactly as received, so nothing is converted behind your back and toArray() returns
the original. To work with them as dates, convert them where you need to:
$domain = $openProvider->domain()->getDomain(123)->data; $expires = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', (string) $domain->expirationDate, new DateTimeZone('Europe/Amsterdam'), );
Experimental operations
A few operations are documented only on the developer portal and not in Openprovider's published
specification: Mailcow white-label and the reseller signup. They are marked @experimental in
their docblocks and in the API reference. They work today, but Openprovider may
change them without notice.
use Ashio\OpenProvider\Model\Portal\McWhiteLabelWhiteLabelRetrieveWhiteLabelDomainRequest; $whiteLabel = $openProvider->mailcow()->retrieveWhiteLabelDomain(new McWhiteLabelWhiteLabelRetrieveWhiteLabelDomainRequest());
Leaving /v1beta
The SDK uses /v1, which behaves the same as /v1beta. If you need a transition period, the old
path is one setting away:
use Ashio\OpenProvider\Client\ClientConfig; use Ashio\OpenProvider\Client\Environment; $legacy = new ClientConfig(Environment::Production, basePath: '/v1beta'); // until 30 June 2027
API reference
Every service and operation, generated from the code. Open a service to see its methods.
Operations marked paged work with Paginator::all(); experimental ones are
documented only by the portal. The full parameter list of each method is
in its docblock, which your IDE shows.
$openProvider->auth() · Auth · 3 operations
| Method | Endpoint | Summary |
|---|---|---|
login() |
POST /auth/login |
Login with username and password |
createResellerPublic() |
POST /resellers/signup |
Sign up a new reseller (public) experimental |
generateLoginURL() |
POST /spam-expert/generate-login-url |
Generate login url |
$openProvider->billing() · Billing · 3 operations
| Method | Endpoint | Summary |
|---|---|---|
listInvoices() |
GET /invoices |
List invoices paged |
listPayments() |
GET /payments |
List payments paged |
listTransactions() |
GET /transactions |
List transactions paged |
$openProvider->dns() · DNS · 21 operations
| Method | Endpoint | Summary |
|---|---|---|
createToken() |
POST /dns/domain-token |
Create token |
listNameservers() |
GET /dns/nameservers |
List nameservers paged |
createNameserver() |
POST /dns/nameservers |
Create nameserver |
listGroups() |
GET /dns/nameservers/groups |
List groups paged |
createGroup() |
POST /dns/nameservers/groups |
Create group |
getGroup() |
GET /dns/nameservers/groups/{ns_group} |
Get group |
updateGroup() |
PUT /dns/nameservers/groups/{ns_group} |
Update group |
deleteGroup() |
DELETE /dns/nameservers/groups/{ns_group} |
Delete group |
getNameserver() |
GET /dns/nameservers/{name} |
Get nameserver |
updateNameserver() |
PUT /dns/nameservers/{name} |
Update nameserver |
deleteNameserver() |
DELETE /dns/nameservers/{name} |
Delete nameserver |
listTemplates() |
GET /dns/templates |
List templates paged |
createTemplate() |
POST /dns/templates |
Create template |
getTemplate() |
GET /dns/templates/{id} |
Get template |
deleteTemplate() |
DELETE /dns/templates/{id} |
Delete template |
listZones() |
GET /dns/zones |
List zones paged, max 500 |
createZone() |
POST /dns/zones |
Create zone |
getZone() |
GET /dns/zones/{name} |
Get zone |
updateZone() |
PUT /dns/zones/{name} |
Update zone |
deleteZone() |
DELETE /dns/zones/{name} |
Delete zone |
listZoneRecords() |
GET /dns/zones/{name}/records |
List zone records paged, max 500 |
$openProvider->domain() · Domain · 21 operations
| Method | Endpoint | Summary |
|---|---|---|
listDomains() |
GET /domains |
List domains paged, max 1000 |
createDomain() |
POST /domains |
Create domain |
getAdditionalData() |
GET /domains/additional-data |
Get additional data |
getData() |
GET /domains/additional-data/customers |
Get data |
checkDomain() |
POST /domains/check |
Check domain |
getPrice() |
GET /domains/prices |
Get price |
suggestNameDomain() |
POST /domains/suggest-name |
Suggest name domain |
tradeDomain() |
POST /domains/trade |
Trade domain |
transferDomain() |
POST /domains/transfer |
Transfer domain |
getDomain() |
GET /domains/{id} |
Get domain |
updateDomain() |
PUT /domains/{id} |
Update domain |
deleteDomain() |
DELETE /domains/{id} |
Delete domain |
getAuthCode() |
GET /domains/{id}/authcode |
Get auth code |
resetAuthCode() |
POST /domains/{id}/authcode/reset |
Reset auth code |
tryAgainLastOperation() |
POST /domains/{id}/last-operation/restart |
Try again last operation |
renewDomain() |
POST /domains/{id}/renew |
Renew domain |
restoreDomain() |
POST /domains/{id}/restore |
Restore domain |
approveTransfer() |
POST /domains/{id}/transfer/approve |
Approve transfer |
sendFoa1() |
POST /domains/{id}/transfer/send-foa1 |
Send foa1 |
listTlds() |
GET /tlds |
List tlds paged |
getTld() |
GET /tlds/{name} |
Get tld |
$openProvider->easyDmarc() · EasyDmarc · 6 operations
| Method | Endpoint | Summary |
|---|---|---|
getEasyDmarc() |
GET /easydmarcs |
Get easy dmarc |
createEasyDmarc() |
POST /easydmarcs |
Create easy dmarc |
listEasyDmarc() |
GET /easydmarcs/list |
List easy dmarc paged |
deleteEasyDmarc() |
DELETE /easydmarcs/{id} |
Delete easy dmarc |
retryEasyDmarc() |
POST /easydmarcs/{id}/retry |
Retry easy dmarc |
getEasyDmarcSso() |
GET /easydmarcs/{id}/sso |
Get easy dmarc sso |
$openProvider->emailTemplate() · Email template · 4 operations
| Method | Endpoint | Summary |
|---|---|---|
listEmails() |
GET /emails |
List emails paged |
createEmail() |
POST /emails |
Create email |
updateEmail() |
PUT /emails/{id} |
Update email |
deleteEmail() |
DELETE /emails/{id} |
Delete email |
$openProvider->license() · License · 15 operations
| Method | Endpoint | Summary |
|---|---|---|
listLicensesWithPriceVersion() |
GET /licenses |
List licenses with price version paged |
resetHwid() |
POST /licenses/hwids/reset/{product}/{key_id} |
Reset hwid |
listItems() |
GET /licenses/items |
List items paged |
listPleskLicenses() |
GET /licenses/plesk |
List plesk licenses paged |
createPleskLicense() |
POST /licenses/plesk |
Create plesk license |
getPleskKey() |
GET /licenses/plesk/key/{key_id} |
Get plesk key |
getPleskLicense() |
GET /licenses/plesk/{key_id} |
Get plesk license |
updatePleskLicense() |
PUT /licenses/plesk/{key_id} |
Update plesk license |
deletePleskLicense() |
DELETE /licenses/plesk/{key_id} |
Delete plesk license |
listVirtuozzoLicenses() |
GET /licenses/virtuozzo |
List virtuozzo licenses paged |
createVirtuozzoLicense() |
POST /licenses/virtuozzo |
Create virtuozzo license |
getVirtuozzoLicense() |
GET /licenses/virtuozzo/{key_id} |
Get virtuozzo license |
updateVirtuozzoLicense() |
PUT /licenses/virtuozzo/{key_id} |
Update virtuozzo license |
deleteVirtuozzoLicense() |
DELETE /licenses/virtuozzo/{key_id} |
Delete virtuozzo license |
getVirtuozzoKey() |
GET /licenses/virtuozzo/{key_id}/key |
Get virtuozzo key |
$openProvider->resellerCustomer() · Reseller/Customer · 20 operations
| Method | Endpoint | Summary |
|---|---|---|
listContacts() |
GET /contacts |
List contacts paged |
createContact() |
POST /contacts |
Create contact |
getContact() |
GET /contacts/{id} |
Get contact |
updateContact() |
PUT /contacts/{id} |
Update contact |
deleteContact() |
DELETE /contacts/{id} |
Delete contact |
listCustomers() |
GET /customers |
List customers paged |
createCustomer() |
POST /customers |
Create customer |
listDomainEmailVerifications() |
GET /customers/verifications/emails/domains |
List domain email verifications paged |
restartEmailVerification() |
POST /customers/verifications/emails/restart |
Restart email verification |
startEmailVerification() |
POST /customers/verifications/emails/start |
Start email verification |
getCustomer() |
GET /customers/{handle} |
Get customer |
updateCustomer() |
PUT /customers/{handle} |
Update customer |
deleteCustomer() |
DELETE /customers/{handle} |
Delete customer |
getReseller() |
GET /resellers |
Get reseller |
getSettings() |
GET /resellers/settings |
Get settings |
getStatistics() |
GET /resellers/statistics |
Get statistics |
updateReseller() |
PUT /resellers/{id} |
Update reseller |
listTags() |
GET /tags |
List tags |
createTag() |
POST /tags |
Create tag |
deleteTag() |
DELETE /tags |
Delete tag |
$openProvider->spamExperts() · Spam Experts · 4 operations
| Method | Endpoint | Summary |
|---|---|---|
createDomain() |
POST /spam-expert/domains |
Create domain |
getDomain() |
GET /spam-expert/domains/{domain_name} |
Get domain |
updateDomain() |
PUT /spam-expert/domains/{domain_name} |
Update domain |
deleteDomain() |
DELETE /spam-expert/domains/{domain_name} |
Delete domain |
$openProvider->ssl() · SSL · 15 operations
| Method | Endpoint | Summary |
|---|---|---|
listApproverEmails() |
GET /ssl/approver-emails |
List approver emails |
createCsr() |
POST /ssl/csr |
Create csr |
decodeCsr() |
POST /ssl/csr/decode |
Decode csr |
listOrders() |
GET /ssl/orders |
List orders paged, max 1000 |
createOrder() |
POST /ssl/orders |
Create order |
getOrder() |
GET /ssl/orders/{id} |
Get order |
updateOrder() |
PUT /ssl/orders/{id} |
Update order |
updateApproverEmailAddress() |
PUT /ssl/orders/{id}/approver-email |
Update approver email address |
resendApproverEmail() |
POST /ssl/orders/{id}/approver-email/resend |
Resend approver email |
cancelOrder() |
POST /ssl/orders/{id}/cancel |
Cancel order |
createOtpToken() |
POST /ssl/orders/{id}/otp-tokens |
Create otp token |
reissueOrder() |
POST /ssl/orders/{id}/reissue |
Reissue order |
renewOrder() |
POST /ssl/orders/{id}/renew |
Renew order |
listProducts() |
GET /ssl/products |
List products paged |
getProduct() |
GET /ssl/products/{id} |
Get product |
$openProvider->mailcow() · Mailcow · 28 operations
| Method | Endpoint | Summary |
|---|---|---|
listAlias() |
GET /mailcow/alias |
List alias paged |
addAlias() |
POST /mailcow/alias |
Add alias |
editAlias() |
POST /mailcow/alias/edit/{id} |
Edit alias |
getAlias() |
GET /mailcow/alias/{id} |
Get alias |
deleteAlias() |
DELETE /mailcow/alias/{id} |
Delete alias |
getMailCowDkim() |
GET /mailcow/dkim |
Get Dkim records for the domain. |
createMailCowDkim() |
POST /mailcow/dkim |
Get Dkim records for the domain. |
deleteMailCowDkim() |
DELETE /mailcow/dkim |
Delete Dkim records for a domain. |
addDomain() |
POST /mailcow/domains |
Add domain |
deleteMailcowDomain() |
DELETE /mailcow/domains |
Delete domain |
listDomain() |
GET /mailcow/domains/list |
List domain paged |
editMailbox() |
POST /mailcow/mailbox/edit |
Edit mailbox |
modifyMailboxName() |
POST /mailcow/mailbox/name/modify |
Modify mailbox name |
increaseMailboxQuota() |
POST /mailcow/mailbox/quota/increase |
Increase mailbox quota |
listOrder() |
GET /mailcow/orders |
List order paged |
createMailcowOrder() |
POST /mailcow/orders |
Create order |
assignMailbox() |
POST /mailcow/orders/assign |
Assign mailbox |
retainMailbox() |
POST /mailcow/orders/retain |
Retain mailbox |
getMailcowOrder() |
GET /mailcow/orders/{id} |
Get order |
deleteOrder() |
DELETE /mailcow/orders/{id} |
Delete order |
getMailboxPassword() |
GET /mailcow/orders/{id}/password |
Get mailbox password |
getMailcowStatistics() |
GET /mailcow/statistics |
Get mailcow statistics |
getTimeLimitedAlias() |
GET /mailcow/time-limited/alias |
Get time limited alias |
addTimeLimitedAlias() |
POST /mailcow/time-limted/alias |
Add time limited alias |
retrieveWhiteLabelDomain() |
POST /mailcow/white-labels/domain |
Retrieve white label domain experimental |
upsertWhiteLabelDomain() |
POST /mailcow/white-labels/domain/upsert |
Upsert white label domain experimental |
retrieveWhiteLabelLogo() |
POST /mailcow/white-labels/logo |
Retrieve white label logo experimental |
modifyWhiteLabelLogo() |
POST /mailcow/white-labels/logo/modify |
Modify white label logo experimental |
How it is built
flowchart LR
subgraph sources ["Openprovider (pinned, sha256)"]
A["api-documentation<br/>per-service Swagger files"]
B["developer portal<br/>spec"]
end
A --> M["tools/spec-merge<br/>one /v1 spec"]
B --> M
O["spec/overrides.json<br/>each decision with a reason"] --> M
M --> S["spec/openprovider.json"]
S --> G["tools/generate"]
BH["spec/behavior.json<br/>token, limits, error classes"] --> G
G --> SRC["src/Model · src/Service<br/>OpenProvider · ErrorCatalog · README"]
SRC --> T{"gates"}
S --> T
Loading
- Sources.
tools/spec-syncdownloads Openprovider's per-service Swagger files and the developer portal's spec, and pins their bytes inspec/SHA256SUMS. A weekly job opens an issue when upstream changes. - Merge.
tools/spec-mergebuilds one/v1spec with the portal's operations and the union of both sources' fields. A conflict between them must be settled inspec/overrides.json, with a reason. The merge is reproducible to the byte. - Generate.
tools/generatewrites the models, the services, the operation registry, the error catalog and the generated parts of this README. The generated code carries an@generatedheader, and CI fails if regenerating it changes a single byte. - Hand-written core. The transport, authentication, request encoding, responses, exceptions
and pagination live outside the generated folders, in
src/Client,src/Auth,src/Http,src/Response,src/Exceptionandsrc/Pagination.
Quality gates
Each gate runs in CI on every pull request and can fail it.
| Gate | What it guarantees | How |
|---|---|---|
| S1 registry | Every operation in the spec has exactly one method, and no public method is anything else | tests/Service/OperationRegistryTest.php |
| S2 conformance | No divergence between what each method sends and reads and what the spec declares; the audit is itself tested against bugs planted from 2.x | composer conformance |
| One HTTP contract test for each of the 140 operations: method, path, query, body, token and response mapping | tests/Service/OperationContractTest.php |
|
| S3 models | Each of the 306 models round-trips seeded random payloads built from its schema, plus edge cases | tests/Model/ModelRoundTripTest.php |
| S4 tests | Line coverage of at least 95% and a mutation score (MSI) of at least 85% | composer test:coverage, Infection |
| S5 types | PHPStan at level max and Psalm at level 1, both with no baseline | composer quality |
| S6 compatibility | PHP 8.2, 8.3, 8.4, 8.5, each with the lowest and the highest dependencies | CI matrix |
| S7 reality | The SDK against the real sandbox | composer smoke, run manually |
| S8 honesty | The badges, the counts, the API reference and the error and gate tables of this README are generated, and every example in it is executed | composer spec:verify, tests/Readme |
PHPUnit itself fails on any warning, notice or deprecation.
Development
composer quality # php-cs-fixer, PHPStan, Psalm, PHPUnit: all must pass composer spec:verify # pinned inputs, reproducible merge, every generated file up to date composer conformance # the S2 audit composer test:coverage # PHPUnit with coverage, then the 95% line gate composer smoke # the sandbox suite; needs OPENPROVIDER_SANDBOX_USERNAME and _PASSWORD
Never edit generated code by hand (src/Model, src/Service, src/OpenProvider.php,
src/Exception/ErrorCatalog.php, or the generated regions of this README): change the inputs and
run php tools/generate errors models operations readme. CONTRIBUTING.md has
the workflow, and AGENTS.md has the contract for coding agents.
Security
- Passwords and tokens are held in a
Secretthat prints as********in strings,var_dump,print_r,var_export, JSON and exception traces, and cannot be serialized. Constructor arguments that carry them are marked#[\SensitiveParameter]. - Every request uses HTTPS. The environment is an enum, so a typo cannot fall back to production.
- To report a vulnerability, use a private security advisory, not a public issue. See SECURITY.md.
Versioning and support
- The SDK follows Semantic Versioning. Generated code changes only through a pinned spec update, and each update is recorded in the changelog.
- 3.0 is a full rewrite with a new namespace (
Ashio\OpenProvider\). It replaces 2.x and offers no compatibility layer. - Each PHP version is supported while it is tested in CI; the badge at the top shows the range.
- Openprovider's
/v1betacloses on 30 June 2027. The SDK has used/v1since 3.0.
License
MIT © Marco Dantas. See LICENSE.
The Openprovider specifications in spec/ are Openprovider's, are not covered by this license and
are not part of the Composer package; see NOTICE. Openprovider is a trademark of
Openprovider B.V. This project is independent and is not affiliated with or endorsed by
Openprovider.