Registrar Plugin Development
Billmora uses a Lifecycle-Driven Architecture for its registrar ecosystem. Developing a Registrar plugin allows you to integrate any third-party domain registrar directly into Billmora.
Because of this architecture, your plugin never needs to directly manipulate the registrant status. Billmora core engine handles all status transitions (pending → active → expired), billing, and event dispatching automatically. Your only responsibility is to communicate with the registrar API.
1. Directory Structure & Namespace
Faster Development & Distribution with CLI
We highly recommend using the Billmora Artisan CLI to scaffold and package your plugin.
# Scaffold the plugin boilerplate structure
php artisan billmora:plugin:make myplugin --type=registrar
# Package the plugin into a ZIP file for distribution
php artisan billmora:plugin:export myplugin --type=registrarRegistrar plugins must reside within the plugin/Registrars/ directory. If you are building a registrar called Example, your directory layout must look like this:
plugin/
└── Registrars/
└── Example/
├── ExampleRegistrar.php
├── plugin.json
└── database/
└── migrations/Consistent with PSR-4 standards, your plugin namespace should match the directory structure: namespace Plugins\Registrars\Example;
2. The plugin.json Manifest
Every plugin requires a plugin.json manifest file. This file tells Billmora core engine how to discover and load your registrar. Ensure the format strictly follows this structure:
{
"name": "Example Registrar",
"provider": "Example",
"type": "registrar",
"version": "1.0.0",
"description": "Register and manage domains via Example API.",
"author": "Your Name / Team",
"icon": "https://url-to-your-registrar-logo.png"
}Configuration Metrics
type: Must strictly be"registrar".provider: The unique identifier/slug for your registrar plugin.icon: An absolute URL to the registrar logo, which will be displayed in the Admin Panel.
3. The Main Plugin Class
Your registrar main PHP class must extend App\Support\AbstractPlugin and implement the App\Contracts\RegistrarInterface.
<?php
namespace Plugins\Registrars\Example;
use App\Contracts\RegistrarInterface;
use App\Support\AbstractPlugin;
use App\Models\Registrant;
use App\Exceptions\RegistrarException;
class ExampleRegistrar extends AbstractPlugin implements RegistrarInterface
{
// Implementation comes here...
}4. Admin Configuration (getConfigSchema)
You do not need to build any HTML forms for your plugin admin settings. Billmora automatically renders the settings UI in the Admin Panel based on the schema you provide.
Use the getConfigSchema() method to define the credentials your registrar requires to communicate with the remote provider.
Schema Documentation
Billmora supports an extensive library of UI components (Selects, Toggles, Radios, Checkboxes, etc.). Please read the Plugin Reference Schema Guide to see the full list of supported fields and properties.
public function getConfigSchema(): array
{
return [
"api_key" => [
"type" => "password",
"label" => "API Key",
"rules" => "required|string"
],
"sandbox" => [
"type" => "boolean",
"label" => "Sandbox Mode",
"default" => true,
"rules" => "boolean"
]
];
}TIP
You can easily retrieve these values anywhere in your class later using $this->getInstanceConfig("api_key");.
If your registrar has no global configuration, return an empty array.
5. Connection Testing (testConnection)
The testConnection() method is called when an admin saves the plugin configuration in the Admin Panel. This allows you to validate credentials and API connectivity before the plugin is activated.
public function testConnection(array $config): bool
{
if (empty($config["api_key"])) {
throw new RegistrarException("API Key is required.");
}
// Test API connectivity
$response = Http::withHeaders([
"Authorization" => "Bearer " . $config["api_key"],
])->get($this->url("/account/test"));
if (!$response->successful()) {
throw new RegistrarException(
"Failed to connect to registrar API: " . $response->status(),
["response" => $response->json() ?: $response->body()]
);
}
return true;
}WARNING
Always throw RegistrarException for connection failures. This ensures the error message is displayed to the admin and the full technical details are logged for debugging.
6. Domain Availability (checkAvailability)
The checkAvailability() method is called when a user searches for a domain during the checkout process. Return an array indicating availability, premium status, and optional pricing.
public function checkAvailability(string $domain): array
{
$response = Http::withHeaders($this->headers())
->get($this->url("/domains/check"), [
"domain" => $domain,
]);
if (!$response->successful()) {
throw new RegistrarException(
"Failed to check domain availability",
["response" => $response->body()]
);
}
$data = $response->json();
return [
"available" => $data["available"] ?? false,
"premium" => $data["premium"] ?? false,
"price" => $data["price"] ?? null,
];
}Return Value Reference
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
available | bool | ✅ | — | true if the domain is available for registration |
premium | bool | false | true if the domain is a premium domain with special pricing | |
price | float? | null | The registration price (only required for premium domains) |
7. Domain Registration (create)
The create() method is called when a new domain registration is purchased. This method should register the domain with the registrar and store any necessary identifiers in the registrant configuration.
public function create(Registrant $registrant): void
{
$sandbox = $this->getInstanceConfig("sandbox", false);
$payload = [
"domain" => $registrant->domain,
"years" => $registrant->years,
"registrant" => [
"name" => $registrant->user->name,
"email" => $registrant->user->email,
"phone" => $registrant->user->phone,
"address" => $registrant->user->address,
"city" => $registrant->user->city,
"state" => $registrant->user->state,
"country" => $registrant->user->country,
"postal_code" => $registrant->user->postal_code,
],
"nameservers" => $registrant->nameservers ?? [],
];
$response = Http::withHeaders($this->headers())
->post($this->url("/domains/register"), $payload);
if (!$response->successful()) {
throw new RegistrarException(
"Failed to register domain " . $registrant->domain,
["response" => $response->body()]
);
}
$data = $response->json();
// Store the registrar domain ID for future operations
$configuration = $registrant->configuration ?? [];
$configuration["registrar_domain_id"] = $data["id"];
$registrant->update([
"configuration" => $configuration
]);
Log::info("[Example] Domain registered", [
"domain" => $registrant->domain,
"registrar_id" => $data["id"],
]);
}TIP
Always store the registrar domain ID in the registrant configuration. This is required for subsequent operations like renewals, transfers, and nameserver updates.
8. Domain Transfer (transfer)
The transfer() method is called when a user initiates a domain transfer from another registrar. The EPP (Extensible Provisioning Protocol) code is provided as the second parameter.
public function transfer(Registrant $registrant, string $eppCode): void
{
$payload = [
"domain" => $registrant->domain,
"epp_code" => $eppCode,
"registrant" => [
"name" => $registrant->user->name,
"email" => $registrant->user->email,
// ... other contact details
],
];
$response = Http::withHeaders($this->headers())
->post($this->url("/domains/transfer"), $payload);
if (!$response->successful()) {
throw new RegistrarException(
"Failed to initiate transfer for " . $registrant->domain,
["response" => $response->body()]
);
}
$data = $response->json();
$configuration = $registrant->configuration ?? [];
$configuration["registrar_domain_id"] = $data["id"];
$configuration["transfer_status"] = "pending";
$registrant->update([
"configuration" => $configuration
]);
}WARNING
Domain transfers can take several days to complete. Your plugin should handle the transfer status appropriately and use syncStatus() to check for completion.
9. Domain Renewal (renew)
The renew() method is called when a domain renewal is processed. The second parameter specifies the number of years to renew.
public function renew(Registrant $registrant, int $years = 1): void
{
$registrarId = $registrant->configuration["registrar_domain_id"] ?? null;
if (!$registrarId) {
throw new RegistrarException(
"Registrar domain ID not found for " . $registrant->domain
);
}
$response = Http::withHeaders($this->headers())
->post($this->url("/domains/" . $registrarId . "/renew"), [
"years" => $years,
]);
if (!$response->successful()) {
throw new RegistrarException(
"Failed to renew domain " . $registrant->domain,
["response" => $response->body()]
);
}
Log::info("[Example] Domain renewed", [
"domain" => $registrant->domain,
"years" => $years,
]);
}10. Nameserver Management
Getting Nameservers (getNameservers)
Return the current nameservers for a domain.
public function getNameservers(Registrant $registrant): array
{
$registrarId = $registrant->configuration["registrar_domain_id"] ?? null;
if (!$registrarId) {
return ["ns1.example.com", "ns2.example.com"];
}
$response = Http::withHeaders($this->headers())
->get($this->url("/domains/" . $registrarId . "/nameservers"));
if (!$response->successful()) {
throw new RegistrarException(
"Failed to fetch nameservers for " . $registrant->domain,
["response" => $response->body()]
);
}
return $response->json("nameservers", []);
}Return Type
This method must return array<int, string> — an array of nameserver strings.
Setting Nameservers (setNameservers)
Update the nameservers for a domain.
public function setNameservers(Registrant $registrant, array $nameservers): void
{
$registrarId = $registrant->configuration["registrar_domain_id"] ?? null;
if (!$registrarId) {
throw new RegistrarException(
"Registrar domain ID not found for " . $registrant->domain
);
}
$response = Http::withHeaders($this->headers())
->put($this->url("/domains/" . $registrarId . "/nameservers"), [
"nameservers" => $nameservers,
]);
if (!$response->successful()) {
throw new RegistrarException(
"Failed to update nameservers for " . $registrant->domain,
["response" => $response->body()]
);
}
// Update local configuration
$configuration = $registrant->configuration ?? [];
$configuration["nameservers"] = $nameservers;
$registrant->update([
"configuration" => $configuration
]);
}Parameter Type
The $nameservers parameter must be array<int, string> — an array of nameserver strings.
11. Status Synchronization (syncStatus)
The syncStatus() method is called periodically to sync the domain status from the registrar. This ensures Billmora records stay up-to-date with the registrar actual state.
public function syncStatus(Registrant $registrant): array
{
$registrarId = $registrant->configuration["registrar_domain_id"] ?? null;
if (!$registrarId) {
return [
"status" => $registrant->status,
"expires_at" => $registrant->expires_at?->toDateTimeString(),
];
}
$response = Http::withHeaders($this->headers())
->get($this->url("/domains/" . $registrarId));
if (!$response->successful()) {
throw new RegistrarException(
"Failed to sync status for " . $registrant->domain,
["response" => $response->body()]
);
}
$data = $response->json();
// Map registrar status to Billmora status
$statusMap = [
"active" => "active",
"expired" => "expired",
"pending_transfer" => "pending_transfer",
"transferred" => "active",
"cancelled" => "cancelled",
];
$status = $statusMap[$data["status"]] ?? $registrant->status;
return [
"status" => $status,
"expires_at" => $data["expiration_date"] ?? $registrant->expires_at?->toDateTimeString(),
];
}Return Value Reference
| Property | Type | Description |
|---|---|---|
status | string | The synced domain status (active, expired, pending_transfer, cancelled) |
expires_at | string? | The expiration date in datetime format (YYYY-MM-DD HH:MM:SS) |
TIP
Billmora automatically updates the registrant status and expiration date based on the values returned by this method.
12. Error Handling & Auditing
For external API failures, always use App\Exceptions\RegistrarException instead of the generic \Exception.
This allows Billmora core engine to:
- Display a concise, user-friendly message in the UI alert.
- Record the full technical response body in the system audit logs for debugging.
use App\Exceptions\RegistrarException;
// ...
if (!$response->successful()) {
throw new RegistrarException(
"Failed to register domain. Status: " . $response->status(),
["response" => $response->json() ?: $response->body()]
);
}13. Best Practices
Logging
Use Laravel Log facade extensively. Billmora admin interface surfaces these logs for debugging.
Log::info("[Example] Checking availability", ["domain" => $domain]);
Log::error("[Example] Registration failed", ["response" => $response->body()]);
Log::debug("[Example] API Payload", ["payload" => $payload]);Storing Registrar IDs
Always store the registrar domain ID in the registrant configuration after successful registration or transfer. This is critical for all subsequent operations.
$configuration = $registrant->configuration ?? [];
$configuration["registrar_domain_id"] = $data["id"];
$registrant->update(["configuration" => $configuration]);Handling Sandbox Mode
Many registrars provide sandbox environments for testing. Use the configuration to switch between sandbox and production endpoints.
private function url(string $path): string
{
$sandbox = $this->getInstanceConfig("sandbox", false);
$baseUrl = $sandbox
? "https://api-sandbox.example.com"
: "https://api.example.com";
return $baseUrl . $path;
}Database Migrations
If your registrar plugin requires its own database tables, place migrations in database/migrations/. All tables must use the pr_ prefix.
TIP
See the Plugin Conventions & Standards guide for full details on table prefixes, migration naming, and model configuration.
14. Optional Hooks
Optional hooks are methods that your plugin may implement to extend default behavior. They are not part of RegistrarInterface and are never required. Billmora's core checks for their existence via method_exists() before calling them.
validateBeforeCart(string $domain, string $type, ?string $eppCode): ?string
Called before a domain is added to the cart, after the client has selected the registration period (or entered an EPP code for transfers). Use this hook to perform any custom validation — either internal or by calling an external API — before the item is confirmed into the cart.
Parameters:
| Parameter | Type | Description |
|---|---|---|
$domain | string | The full domain name (e.g., example.com). |
$type | string | Either 'register' or 'transfer'. |
$eppCode | string|null | The EPP code provided by the client. null for registrations. |
Return value:
- Return
nullto allow the cart action to proceed. - Return a
stringerror message to block it — Billmora will display this message as a flash error to the client.
WARNING
If this method throws an exception, Billmora will block the cart action and display a generic "Validation service is currently unavailable" error to the client. Do not throw exceptions for expected validation failures — use the string return value instead.
public function validateBeforeCart(string $domain, string $type, ?string $eppCode): ?string
{
// Example: block domains on a blacklist
if ($this->isDomainBlacklisted($domain)) {
return "The domain \"{$domain}\" is not available for registration through this provider.";
}
// Example: validate EPP code format before attempting transfer
if ($type === 'transfer' && $eppCode && !preg_match('/^[a-zA-Z0-9\-]{6,32}$/', $eppCode)) {
return 'The EPP code format is invalid. Please check and try again.';
}
return null;
}15. Conclusion
By implementing the RegistrarInterface methods and letting Billmora core engine handle the billing, invoicing, and customer management, you can build powerful domain registration integrations with minimal boilerplate. Your plugin only needs to focus on speaking to the registrar API — Billmora takes care of the rest!
