transip/transip-api-php
PHP client for the TransIP REST API, letting you manage domains, DNS records, VPS instances, and other services from your PHP application with simple, authenticated API calls.
1. Install and generate a key pair. The client itself has no Laravel service provider — you wire it up yourself.
composer require transip/transip-api-php
openssl genrsa -out storage/app/transip/private.key 4096
openssl rsa -in storage/app/transip/private.key -pubout -out storage/app/transip/public.key
Paste the public key into the TransIP control panel (API settings), enable the API for your account, and whitelist the IP you'll call from. Nothing works until that whitelist entry exists.
2. Configure it. Keep the private key on disk, not in .env — env vars mangle the PEM newlines.
// config/services.php
'transip' => [
'login' => env('TRANSIP_LOGIN'),
'private_key' => storage_path('app/transip/private.key'),
'read_only' => env('TRANSIP_READ_ONLY', true),
],
3. Bind a singleton that refreshes its own token. Tokens are short-lived (~30 min), so cache them rather than re-authenticating on every call.
// app/Providers/TransipServiceProvider.php
$this->app->singleton(TransIPAPI::class, fn () => new TransIPAPI(
config('services.transip.login'),
file_get_contents(config('services.transip.private_key')),
true, // whitelist-only token
config('services.transip.read_only'),
));
$this->app->singleton(TransipClient::class, fn ($app) => new TransipClient(
$app->make(TransIPAPI::class)
));
4. First use case: read your DNS. Start read-only. Dump every record for one domain and eyeball the shape of the data before you write anything.
$api->setToken(Cache::remember('transip:token', now()->addMinutes(25), fn () => $api->getToken()));
foreach ($api->dns()->getByDomainName('example.com') as $entry) {
// $entry->getName(), ->getExpire(), ->getType(), ->getContent()
}
The entity objects (Transip\Api\Library\Entity\DnsEntry, Domain, Vps, SshKey) are plain getter/setter bags — that's the API surface you'll actually be programming against.
Wrap the client, never inject it directly into controllers/jobs. The TransIPAPI object is stateful (setToken() mutates it) and throws raw exceptions. One thin wrapper gives you a fake-able seam for tests plus a single place to handle token refresh and retries.
final class TransipClient
{
public function __construct(private TransIPAPI $api) {}
private function api(): TransIPAPI
{
$this->api->setToken(
Cache::remember('transip:token', now()->addMinutes(25), fn () => $this->api->getToken())
);
return $this->api;
}
/** @return DnsEntry[] */
public function records(string $domain): array
{
return Cache::remember("transip:dns:{$domain}", 60, fn () => $this->api()->dns()->getByDomainName($domain));
}
}
Reconcile, don't mutate. dns()->update($domain, $entries) replaces the entire record set for the domain. The safe pattern for "make sure this A record exists" is read → modify the array → write the whole thing back, inside a transaction-like job:
$records = $api->dns()->getByDomainName($domain);
$records = array_filter($records, fn ($r) => !($r->getName() === 'www' && $r->getType() === 'A'));
$records[] = (new DnsEntry())
->setName('www')
->setExpire(300)
->setType('A')
->setContent('203.0.113.10');
$api->dns()->update($domain, array_values($records));
Cache::forget("transip:dns:{$domain}");
Queue every write. DNS and VPS calls are network-bound and rate-limited; a 5xx or 429 in a request cycle is a bad user experience. Use a dedicated queue with $tries = 3, backoff() returning 10/60/300, and failed() logging the domain + intended state so you can replay it.
Cache reads aggressively. Domain lists, record sets, and SSH keys change on human timescales. Cache for 30–60s and bust on write — this also keeps you well clear of rate limits when a dashboard renders.
Good day-to-day use cases. Dynamic DNS updater (scheduled command: fetch public IP → reconcile @ A record), per-tenant subdomain provisioning on signup, SSH key rotation across a fleet (ssh()->getAll() / create / delete), and an ops dashboard that lists expiring domains via domains()->getAll().
Test the wrapper, not the API. Because the client isn't PSR-18-injectable, bind a fake TransipClient into the container in tests and assert on intent. Keep one integration test tagged @group external for real calls.
read_only is true, writes fail at the API — in local dev that's what you want, but it produces confusing 403s if you flip it only in .env and forget to clear config cache.file_get_contents() a real .pem file. If you must use an env var, base64-encode it and decode at boot; a key pasted with literal \n will fail signing with an opaque error.delete() needs an exact match. Deleting a DNS record means matching name, expire, type, and content. Fetch first, pass the entity back — don't reconstruct it from memory and expect it to match.@, not the bare domain. Subdomain labels are relative (www), not FQDNs. Getting this wrong silently creates a duplicate record rather than erroring.content round-trips exactly as you sent it.update() is destructive by design. There is no per-record PUT; a partial array wipes records. Always read-modify-write, and log the before/after array on failure.Cache::remember on every entry point (as above) avoids this; with Octane, prefer scoped binding over singleton.TransipClient with a logging decorator) if you want request/response visibility.How can I help you explore Laravel packages today?