Skip to content

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 (pendingactiveexpired), 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.

bash
# 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=registrar

Registrar plugins must reside within the plugin/Registrars/ directory. If you are building a registrar called Example, your directory layout must look like this:

text
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:

json
{
  "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
<?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.

php
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.

php
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.

php
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

PropertyTypeRequiredDefaultDescription
availablebooltrue if the domain is available for registration
premiumboolfalsetrue if the domain is a premium domain with special pricing
pricefloat?nullThe 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.

php
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.

php
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.

php
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.

php
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.

php
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.

php
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

PropertyTypeDescription
statusstringThe synced domain status (active, expired, pending_transfer, cancelled)
expires_atstring?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:

  1. Display a concise, user-friendly message in the UI alert.
  2. Record the full technical response body in the system audit logs for debugging.
php
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.

php
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.

php
$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.

php
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:

ParameterTypeDescription
$domainstringThe full domain name (e.g., example.com).
$typestringEither 'register' or 'transfer'.
$eppCodestring|nullThe EPP code provided by the client. null for registrations.

Return value:

  • Return null to allow the cart action to proceed.
  • Return a string error 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.

php
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!