Skip to content

Registrar credentials & the connection profile

A registrar's driver and connection profile are separate — declare credential fields in the manifest, read them safely per call, and verify them with a Test connection probe.

9 min readUpdated Aug 15, 2026
On this page

A registrar driver has to authenticate to a registrar's API — a Namecheap API key, an Enom login, a ResellerClub reseller id + key — and Core has to collect the right credentials to do it. None of that requires a Core edit. Two things are involved and they are deliberately separate: the driver (the signed extension, code only) and the connection profile (the credentials one admin fills in). Your salieno.json's credentials list tells Core how to render the config form, and a helper trait reads those stored values back into your driver on every call. This article covers both halves and how they meet at the "Test connection" probe.

The connection profile: driver and credentials are separate#

A registrar's driver and its connection profile are distinct records with distinct lifecycles. The driver is the signed marketplace extension — code, gated three ways before it is allowed to run (see Security & trust model). The connection profile is a DomainRegister row keyed by registrar_slug, holding the credentials one operator entered for that registrar.

Installing the extension auto-provisions an unconfigured profile under admin → Domain Registrars. It arrives empty: the operator opens it, fills in the credential fields your manifest declared, and clicks Test connection. Until then the registrar is installed but not usable — the fields exist, the values do not. Disabling or uninstalling the extension takes the config form with it; an uninstalled registrar exposes no fields at all.

Every driver method is handed a Domain, and each domain points at the profile it was registered through. Your driver resolves it per call:

php
$registrar = $this->registrarOf($domain);   // the DomainRegister connection profile
$apiKey    = $this->getParam($registrar, 'api_key', '');

The driver never holds credentials in instance state — it is stateless (the registry builds it once, with no constructor, and passes the Domain to every method). Read the profile on every call. See The driver contract for why.

The credentials list builds the config form#

Core renders the Domain Registrars config modal straight from your manifest's credentials list. There is no per-registrar form in Core; the fields, labels, help text, and which are required all come from what you declare. Adding a registrar needs no Core changes.

json
"credentials": [
  { "key": "api_user",  "label": "API User",              "type": "text",     "required": true,
    "help": "Your Namecheap API username (usually your account username)." },
  { "key": "api_key",   "label": "API Key",               "type": "password", "required": true,
    "help": "Profile > Tools > Namecheap API Access." },
  { "key": "username",  "label": "Account Username",      "type": "text",     "required": false,
    "help": "Defaults to the API User when blank." },
  { "key": "client_ip", "label": "Whitelisted Server IP", "type": "text",     "required": true,
    "help": "Your server's outbound IP, whitelisted in Namecheap." }
]

Each entry is a {key, label, type, required, help} object:

FieldWhat it does
keyThe storage key — the exact string your driver passes to getParam($registrar, 'api_key'). Keep it stable across versions; renaming it orphans stored values.
labelThe field label in the config modal. Use the registrar's own vocabulary ("API User", "Reseller ID") so it matches their dashboard.
type"text" for a plain field, "password" for a masked secret.
requiredtrue marks the field mandatory in the form. An optional field (false) may be left blank; give your driver a sensible default when reading it.
helpInline help under the field: where in the registrar's dashboard the value lives. This is the operator's only guidance, so make it specific.

The list is read straight from the manifest by RegistrarRegistry::credentialFields(), which is exactly what the config UI calls. The manifest's credentials keys and your driver's getParam keys are the contract between the form and the code — they must agree. For the full manifest schema see The manifest.

text vs password#

type picks the input and how the value is treated on screen:

  • "text" — a normal input, shown in the clear. Use it for non-secret identifiers: an API username, an account handle, a whitelisted IP.
  • "password" — a masked input. Use it for anything that would let someone act as the account: the API key, a reseller password, a secret token.

type is a UI hint, not the whole of the protection — every credential value is encrypted at rest regardless (see below). But mark secrets "password" so they are never shown on screen or shoulder-surfed while an operator edits the profile.

Reading credentials in the driver#

Extend AbstractRegistrar and you inherit RegistrarModuleTrait, which reads the stored profile for you. Three helpers cover credentials.

getParam($registrar, $key, $default = null)#

Returns one credential the operator entered. It handles the stored {title, value} wrapper — profile values are persisted as objects — so you always get the scalar you declared, or your default when the key was left blank:

php
$apiUser  = (string) $this->getParam($registrar, 'api_user', '');
$apiKey   = (string) $this->getParam($registrar, 'api_key', '');
// optional field: fall back to another credential, not to empty
$username = (string) $this->getParam($registrar, 'username', $apiUser);

Note the username default. The manifest marks it optional and its help says "Defaults to the API User when blank", so the driver makes that real by defaulting to $apiUser. The form and the code have to agree on that behaviour — the help text is a promise the driver keeps.

params($registrar)#

Returns the whole credential object at once, normalised from its stored shape. Reach for it only when you need several values together; prefer getParam for a single field.

isSandbox($registrar)#

Returns the profile's test-mode toggle:

php
$apiUrl = $this->isSandbox($registrar)
    ? 'https://api.sandbox.namecheap.com/xml.response'
    : 'https://api.namecheap.com/xml.response';

`test_mode` is not a credential you declare. Core provides the sandbox toggle on every profile itself; do not add it to your credentials list. Read it with isSandbox($registrar) and point at the registrar's sandbox host when it is on, so an operator can exercise register/transfer against test infrastructure before going live. See Testing your registrar.

Credentials are encrypted and never logged#

DomainRegister.params is encrypted at rest, and the model hides it from serialization, logs, and Livewire snapshots — so a profile's credentials do not leak into a stack trace, a debug dump, or the admin page's wire state. Your side of that contract:

  • Never log a credential. Not the key, not the whole params object. When you log an error, log the registrar's message, not the request that carried the secret.
  • Never put a secret in a URL you control. Namecheap authenticates with query-string credentials, which is the registrar's own design; where a registrar accepts either, prefer the body or a header so the secret does not land in your access log or a fronting proxy.
  • Use `type: "password"` for every secret field so it is masked in the form.

Real-world credential gotchas#

Registrars rarely need just a key. Namecheap is the worked example (registrars/namecheap), and its credential shape shows the traps:

  • `api_user` + `api_key` authenticate the account, username is an optional third value that defaults to api_user, and `client_ip` is a fourth field most APIs never ask for.
  • `client_ip` must be the server's whitelisted outbound IP. Namecheap rejects a call from an IP that is not on its dashboard allowlist with <Error Number="1011150">Invalid request IP</Error>. The driver must send the server's own public outbound IP — the one whitelisted in Profile > Tools > API Access.
  • Resolve it from the credential, never from `request()->ip()`. Register, renew, and transfer run in queue and cron workers, where request()->ip() is 127.0.0.1 (or null); on a web request it is the customer's browser IP. Neither is the server's outbound IP. The driver reads the operator-supplied client_ip param and only falls back to the host's own resolved address:
php
protected function resolveClientIp(?DomainRegister $registrar): string
{
    $configured = trim((string) $this->getParam($registrar, 'client_ip', ''));
    if ($configured !== '' && filter_var($configured, FILTER_VALIDATE_IP)) {
        return $configured;
    }
    // last resort: the host's own resolved IP — never request()->ip()
    // ...
    return '';   // empty → the caller fails loudly and actionably
}

Surface actionable errors, not raw API codes#

When a credential is missing or mis-set, catch it before the API does and return a sentence the operator can act on — a registrar error number tells them nothing. Namecheap's request choke point checks the config first:

php
if ($apiUser === '' || $apiKey === '') {
    return ['ok' => false, 'message' => 'Namecheap API credentials are not configured (ApiUser / ApiKey missing).'];
}
if ($clientIp === '') {
    return ['ok' => false, 'message' => 'Namecheap ClientIp is not configured — set the "client_ip" param to your server\'s whitelisted public IP (Profile > Tools > API Access).'];
}

And when the API does reject the call, surface its real reason rather than a generic failure: parse the registrar's error envelope and pass the detail through, so "Invalid request IP" reaches the operator verbatim instead of being flattened to "request failed". Mask nothing the operator needs in order to fix it — but never echo the credential itself back in the message.

The testConnection probe#

testConnection(DomainRegister $registrar) is the "Test connection" button on the profile. It is the one place credentials, the sandbox toggle, and your API client meet as a single testable path — so implement it as a lightweight, side-effect-free, authenticated call that proves the stored credentials actually work:

php
public function testConnection(DomainRegister $registrar): array
{
    $res = $this->request($registrar, 'namecheap.domains.getList', ['PageSize' => 1, 'Page' => 1], 30);
    if (! $res['ok']) {
        return $this->error($res['message'], null, false);   // no domain → no admin notification
    }
    return $this->success('credentials are valid.');
}

Namecheap uses namecheap.domains.getList with PageSize=1 — the cheapest call that still exercises auth and the ClientIp whitelist, so a success proves the profile is usable and a failure surfaces the true reason (bad key, or "Invalid request IP" when the server IP is not whitelisted). It returns ['success' => bool, 'message' => ...]; the admin sees the message under the button. Pass notify: false to error() here — a failed connection test is the operator's own probe, not a live incident, so it should not raise an admin notification.

testConnection is not a capability — it is not gated, and every registrar should override it. The full method contract (register, transfer, sync, and the rest) lives in The driver contract; this probe is what ties your credentials to it. Once it returns success against the sandbox, walk the same path against live and you have a working connection profile — see Testing your registrar.

Was this article helpful?
Still stuck?Contact support
Registrar credentials & the connection profile · Salieno Docs