Skip to main content
REST API for resellers

API documentation

Automate reselling and managing LJPc hosting and domain names.

A REST API that returns JSON. Register and transfer domains, manage DNS, order and provision hosting and manage client sites, exactly like the reference boilerplate does.


Introduction

The LJPc reseller API lets you sell and manage domain names and hosting fully automatically. It is a REST API that returns JSON over HTTPS.

All requests go to the base URL https://ljpc-hosting.nl with paths under /api. Hosting paths live under /api/hosting/v1, the order and product paths under /api/v1. All amounts are the price your account pays us, including your valuepack, excluding VAT; on top of that, as a reseller you set your own retail price for your customer.

Authentication

Every request authenticates with the X-API-Key header. There is no bearer token or cookie involved. A missing or unknown key returns HTTP 401 with an envelope body.

curl -H "X-API-Key: <your-key>" \
  https://ljpc-hosting.nl/api/hosting/v1/tlds

Your key access. LJPc provisions your key with exactly the access your integration needs. To change what your key can do, get in touch with us.

No key yet? Get in touch to request one.

Response format

Almost every endpoint wraps its payload in the standard envelope. Read .data for the payload on success and .message for the error text on failure. Each response also carries an X-Validation header (a short-lived JWT whose jti matches the body); verifying it is optional.

// Success (200 / 201 / 202)
{ "success": true, "jti": "<uuid>", "data": <payload> }

// Error (4xx / 5xx)
{ "success": false, "jti": "<uuid>", "message": "<reason>" }

Exception: products (RAW JSON)

The order module catalogue and shopping-cart endpoints are public (no X-API-Key) and return RAW JSON, not the envelope. GET /api/v1/products returns {"products": […]} directly, without success, jti or data.

Endpoints

The paths below omit the https://ljpc-hosting.nl prefix in the card titles but show it in the examples. Domain path parameters accept dots.

Domains and TLDs

The TLD catalogue, availability and price checks, and full domain management: register, transfer, update and fetch auth codes.

GET /api/hosting/v1/tlds

Returns the full sellable TLD catalogue with indicative catalogue list prices (EUR) per year. price is null when the catalogue price is unavailable; transfer_supported indicates whether the TLD supports an auth-code transfer. For the exact price of a specific domain including your valuepack, use /domain-names/price.

Required permission: None (valid key only)

Example request

curl -H "X-API-Key: <your-key>" \
  https://ljpc-hosting.nl/api/hosting/v1/tlds

Example response

200 OK
{
  "success": true,
  "jti": "…",
  "data": [
    { "tld": "nl", "price": 3.88, "currency": "EUR", "transfer_supported": true },
    { "tld": "2000.hu", "price": 5, "currency": "EUR", "transfer_supported": true }
  ]
}
GET /api/hosting/v1/domain-names/check

Checks the availability of a domain name. The answer is a string keyed per queried domain (for example available). A TLD we do not sell returns unknown.

Required permission: Check domain availability

Parameter Type Required Description
domain query string Yes The domain name to check. An array is also accepted.

Example request

curl -H "X-API-Key: <your-key>" \
  "https://ljpc-hosting.nl/api/hosting/v1/domain-names/check?domain=example.nl"

Example response

200 OK
{ "success": true, "jti": "…", "data": { "example.nl": "available" } }
GET /api/hosting/v1/domain-names/price

Returns the exact price of a domain name for your account, including your valuepack, as a nested money object per domain, plus a premium flag. An unknown TLD returns null for that key.

Required permission: Check domain price

Parameter Type Required Description
domain query string Yes The domain name to price. An array is also accepted.

Example request

curl -H "X-API-Key: <your-key>" \
  "https://ljpc-hosting.nl/api/hosting/v1/domain-names/price?domain=example.nl"

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "example.nl": {
    "premium": false,
    "price":     { "amount": "11.23", "currency": "EUR" },
    "price_eur": { "amount": "11.23", "currency": "EUR" }
  } } }
GET /api/hosting/v1/domain-names

Lists your own domain names (owner-scoped), paginated.

Required permission: View domains and sites

Parameter Type Required Description
page query int No Page number, default 1.
per_page query int No Items per page, max 100, default 25.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "domains": [
    { "domain": "example.nl", "status": "active",
      "expires_at": "2027-07-02T16:37:57+02:00",
      "auto_renew": true, "transfer_lock": true }
  ],
  "total": 2, "page": 1, "per_page": 25 } }
GET /api/hosting/v1/domain-names/{domain}

Detail of a domain. Live info from our system is merged over the stored row (falling back to the row when our system is temporarily unavailable). status may be a comma-joined status; treat it as opaque. A foreign domain returns 404.

Required permission: View domains and sites

Parameter Type Required Description
domain path Yes The domain name.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "domain": "example.nl", "status": "active",
  "expires_at": "2027-07-02T16:37:57+02:00",
  "nameservers": ["ns1.ljpc.network", "ns2.ljpc.network", "ns3.ljpc.network"],
  "auto_renew": true, "transfer_lock": true,
  "contact_handle": "apitest-owned-0001" } }
POST /api/hosting/v1/domain-names

Registers a new domain name on an owned WHOIS contact. The guard order rejects an unsupported TLD, a duplicate, or an unknown/foreign handle before the domain is registered in our system.

Required permission: Register domains through the API

Parameter Type Required Description
domain body string Yes The domain name to register.
contact_handle body string Yes An owned WHOIS handle.
years body int No 1..10, default 1.
nameservers body array No 2..13 hostnames.
external_reference body string No Your own reference.

Example request

curl -X POST -H "X-API-Key: <your-key>" \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.nl","contact_handle":"apitest-owned-0001","years":1}' \
  https://ljpc-hosting.nl/api/hosting/v1/domain-names

Example response

201 Created
{ "success": true, "jti": "…", "data": {
  "domain": "example.nl", "status": "active",
  "message": "Domain registered successfully" } }

422 { "success": false, "jti": "…", "message": "Domain already exists" }
403 { "success": false, "jti": "…", "message": "Contact handle does not belong to you" }
POST /api/hosting/v1/domain-names/{domain}/transfer

Initiates the transfer of a domain with an auth code to an owned WHOIS contact. Returns a pending_transfer status.

Required permission: Transfer domains through the API

Parameter Type Required Description
domain path Yes The domain to transfer.
auth_code body string Yes The EPP/auth code.
contact_handle body string Yes An owned WHOIS handle.
nameservers body array No Optional nameservers.
external_reference body string No Your own reference.

Example request

curl -X POST -H "X-API-Key: <your-key>" \
  -H "Content-Type: application/json" \
  -d '{"auth_code":"<epp-code>","contact_handle":"apitest-owned-0001"}' \
  https://ljpc-hosting.nl/api/hosting/v1/domain-names/example.nl/transfer

Example response

202 Accepted
{ "success": true, "jti": "…", "data": {
  "domain": "example.nl", "status": "pending_transfer",
  "message": "Domain transfer initiated successfully" } }
PATCH /api/hosting/v1/domain-names/{domain}

Updates only the fields you send (only-changed). An empty body returns 422 "No changes to apply". A foreign domain returns 404.

Required permission: Edit own domains

Parameter Type Required Description
domain path Yes The domain to update.
nameservers body array No 2..4 hostnames.
auto_renew body bool No Toggle auto-renew.
transfer_lock body bool No Toggle the transfer lock.
whois_contact_handle body string No An owned WHOIS handle.

Example request

curl -X PATCH -H "X-API-Key: <your-key>" \
  -H "Content-Type: application/json" \
  -d '{"auto_renew":false}' \
  https://ljpc-hosting.nl/api/hosting/v1/domain-names/example.nl

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "domain": "example.nl", "message": "Domain updated successfully" } }
GET /api/hosting/v1/domain-names/{domain}/authcode

Retrieves the EPP/auth code for an outgoing transfer. 404 when our system does not expose a code or the domain is not yours.

Required permission: View auth codes

Parameter Type Required Description
domain path Yes The domain name.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "domain": "example.nl", "auth_code": "<epp-code>" } }

404 { "success": false, "jti": "…",
      "message": "Auth code is not available for this domain" }

WHOIS contacts

Registrant contacts. Registering or transferring a domain requires an owned WHOIS handle, so create or reuse a contact here first.

GET /api/hosting/v1/contacts

Lists your WHOIS contacts (owner-scoped), paginated.

Required permission: View WHOIS contacts through the API

Parameter Type Required Description
page query int No Page number, default 1.
per_page query int No Items per page, max 100, default 25.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "contacts": [
    { "brand": "default", "name": "Owned Contact",
      "addressLine": ["Teststreet 1"], "postalCode": "1234AB",
      "city": "Testcity", "country": "NL", "email": "owned@example.com",
      "voice": "+31.612345678", "organization": "LJPc",
      "handle": "apitest-owned-0001" }
  ],
  "total": 1, "page": 1, "per_page": 25 } }
GET /api/hosting/v1/contacts/{handle}

Detail of a WHOIS contact. A foreign handle returns 404.

Required permission: View WHOIS contacts through the API

Parameter Type Required Description
handle path Yes The contact handle.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "brand": "default", "name": "Owned Contact",
  "addressLine": ["Teststreet 1"], "postalCode": "1234AB",
  "city": "Testcity", "country": "NL", "email": "owned@example.com",
  "voice": "+31.612345678", "organization": "LJPc",
  "handle": "apitest-owned-0001" } }

404 { "success": false, "jti": "…", "message": "Contact not found" }
POST /api/hosting/v1/contacts

Creates a new WHOIS contact and returns the new handle. Fields are snake_case.

Required permission: Create WHOIS contacts through the API

Parameter Type Required Description
name body string Yes Contact name.
address body string Yes Address as a single string.
country body string Yes ISO-2 country code, exactly 2 characters.
organization body string No Organization name.
postal_code body string No Postal code.
city body string No City.
email body string No Email address.
voice body string No Phone in E.164, for example +31612345678 or +31.612345678.

Example request

curl -X POST -H "X-API-Key: <your-key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"Owned Contact","address":"Teststreet 1","postal_code":"1234AB",
       "city":"Testcity","country":"NL","email":"owned@example.com",
       "voice":"+31612345678"}' \
  https://ljpc-hosting.nl/api/hosting/v1/contacts

Example response

201 Created
{ "success": true, "jti": "…", "data": { "handle": "<new-handle>" } }

422 { "success": false, "jti": "…",
      "message": "The country field must be 2 characters." }
PATCH /api/hosting/v1/contacts/{handle}

Updates a WHOIS contact. Ownership first (404), then validation, then the update in our system. All fields are optional; unset fields keep their stored value.

Required permission: Update WHOIS contacts through the API

Parameter Type Required Description
handle path Yes The contact handle.

Example request

curl -X PATCH -H "X-API-Key: <your-key>" \
  -H "Content-Type: application/json" \
  -d '{"email":"new@example.com"}' \
  https://ljpc-hosting.nl/api/hosting/v1/contacts/apitest-owned-0001

Example response

200 OK
{ "success": true, "jti": "…", "data": { "success": true } }

DNS

Manage the zone records of an owned domain. A record has the shape {type, name, content, ttl, prio}. Managed types are A, AAAA, CNAME, MX, TXT, SRV and CAA. NS records are not managed: they are excluded from GET output and preserved on PUT. name is relative to the zone (@ is the apex), ttl 60..86400, prio required for MX and SRV, otherwise null.

GET /api/hosting/v1/dns/zones/{domain}/records

Fetches the DNS records of an owned domain. 404 when the domain has no zone yet (treat that as an empty set).

Required permission: Manage DNS through the API

Parameter Type Required Description
domain path Yes The owned domain.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "zone": { "domain": "example.nl", "source": "manual" },
  "records": [
    { "type": "A", "name": "@", "content": "192.0.2.10", "ttl": 3600, "prio": null }
  ] } }

404 { "success": false, "jti": "…", "message": "No DNS zone exists for this domain" }
PUT /api/hosting/v1/dns/zones/{domain}/records

Full replace of the managed record types. The first write creates a source=manual zone. An empty array is a valid clear. A zone managed by our hosting control panel returns 409. synced reports whether the change reached our DNS; false is normal until the domain delegates to our nameservers.

Required permission: Manage DNS through the API

Parameter Type Required Description
domain path Yes The owned domain.
records body array Yes The complete desired record set, wrapped in a records key.

Example request

curl -X PUT -H "X-API-Key: <your-key>" \
  -H "Content-Type: application/json" \
  -d '{"records":[
        {"type":"A","name":"@","content":"192.0.2.10","ttl":3600,"prio":null},
        {"type":"MX","name":"@","content":"mail.example.nl","ttl":3600,"prio":10}
      ]}' \
  https://ljpc-hosting.nl/api/hosting/v1/dns/zones/example.nl/records

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "zone": { "domain": "example.nl", "source": "manual" },
  "records": [ … ],
  "synced": false } }

422 { "success": false, "jti": "…",
      "message": "records.0: content must be a valid IPv4 address" }
409 { "success": false, "jti": "…",
      "message": "This zone is managed by our hosting control panel and cannot be edited through the API." }

Hosting

Your hosting sites: list them, read disk usage and generate a single-use SSO URL to the control panel. Everything is owner-scoped.

GET /api/hosting/v1/sites

Lists your hosting sites (owner-scoped), paginated.

Required permission: Show own sites through the API

Parameter Type Required Description
page query int No Page number, default 1.
per_page query int No Items per page, max 100, default 25.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "sites": [
    { "id": 2019, "domain": "example.nl", "status": "active", "disk_space_gb": 10 }
  ],
  "total": 1, "page": 1, "per_page": 25 } }
GET /api/hosting/v1/sites/{id}

Detail of a site including disk usage. disk_quota_bytes is binary GiB (disk_space_gb × 1024³), null when unlimited. disk_used_bytes and email_used_bytes are null when no usage row exists yet. A foreign or unknown id returns 404 (no enumeration).

Required permission: Show own sites through the API

Parameter Type Required Description
id path int Yes The site id.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "id": 2019, "domain": "example.nl", "status": "active",
  "disk_space_gb": 10,
  "disk_used_bytes": 3221225472,
  "email_used_bytes": 1073741824,
  "disk_quota_bytes": 10737418240 } }
GET /api/hosting/v1/sites/{id}/login

Returns a single-use SSO URL to the site control panel. 204 when no URL is available. Owner-scoped: you can only log in to owned or shared sites.

Required permission: Login to sites

Parameter Type Required Description
id path int Yes The site id.

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "url": "https://panel.ljpc.systems/sso?token=<single-use-token>" } }

204 No Content
403 { "success": false, "jti": "…",
      "message": "You don't have permission to login to this site" }

Orders

The product catalogue and placing and tracking orders. An order is created pending and delivered asynchronously; there are no webhooks, so poll GET /api/v1/orders/{id}.

GET /api/v1/products

Product catalogue. RAW JSON, no envelope. Without a key you get the list prices; when you send your X-API-Key, the prices are the price your account pays us per hosting product, including your valuepack. Optional ?category= filter. Prices are decimal strings; monthly_price is null for yearly-only products. Fulfillable hosting packages have a category_identifier of the form hosting_<N>gb, hosting_forward or hosting_dns.

Required permission: None (public, no key)

Parameter Type Required Description
category query string No Filter by category, for example hosting or domains.

Example request

curl https://ljpc-hosting.nl/api/v1/products

Example response

200 OK
{ "products": [
  { "uuid": "afe29fe6-…", "name": "5GB hosting",
    "billing_period_type": "client_choice",
    "monthly_price": "4.99", "yearly_price": "49.90", "price": "4.99",
    "category": "hosting", "category_identifier": "hosting_5gb",
    "required_fields": [] },
  { "uuid": "f7b0f46f-…", "name": "Domein .media",
    "billing_period_type": "yearly_only",
    "monthly_price": null, "yearly_price": "44.99", "price": "44.99",
    "category": "domains", "category_identifier": "media",
    "required_fields": null }
] }
POST /api/v1/orders

Places an order through the restricted path. That path forces status=pending, source=reseller-api, client_id=owner_id=handler_id=the caller, send_invoice=false, send_quote=false, auto_deliver=true, keeps only meta.external_reference and rebuilds each cart_item from a whitelist with server-side pricing. The order id lives at data.order.id.

Required permission: Create orders

Parameter Type Required Description
summary body string No Short description of the order.
meta.external_reference body string No Your own reference (the only meta kept).
order_lines body array Yes Lines of {description, cart_item}. See the cart_item shape below this table.

Example request

curl -X POST -H "X-API-Key: <your-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "New hosting for example.nl",
    "meta": { "external_reference": "ORDER-1234" },
    "order_lines": [
      { "description": "5GB hosting",
        "cart_item": {
          "type": "hosting",
          "product_uuid": "afe29fe6-…",
          "domain": "example.nl",
          "billing_period": "monthly"
        } }
    ] }' \
  https://ljpc-hosting.nl/api/v1/orders

Example response

201 Created
{ "success": true, "jti": "…", "data": { "order": {
  "id": 114, "status": "pending", "source": "reseller-api",
  "summary": "New hosting for example.nl",
  "order_lines": [
    { "description": "5GB hosting", "price": 4.99, "period": 1, "cart_item": { … } }
  ],
  "send_invoice": false, "send_quote": false, "auto_deliver": true,
  "total_price": 4.99, "needs_subscription": true, "needs_payment": true,
  "created_at": "…", "updated_at": "…" } } }

422 { "success": false, "jti": "…",
      "message": "Order line 1: unsupported cart item type 'hosting_upgrade'" }
403 { "success": false, "jti": "…",
      "message": "You can only create orders for yourself" }
GET /api/v1/orders/{id}

Status detail of an order, including per-line provisioning. simple_status (and provisioning.overall) is pending, processing, completed or failed. The per-line state is pending, done or failed. Once a hosting line completes, site_id is filled in; feed it to GET /api/hosting/v1/sites/{id}. A foreign order returns 403, an unknown one 404.

Required permission: View own orders

Parameter Type Required Description
id path int Yes The order id (data.order.id from the create response).

Example response

200 OK
{ "success": true, "jti": "…", "data": {
  "order": { "id": 110, "status": "pending", "source": "reseller-api",
             "order_lines": [ … ], "summary": "…", "total_price": 4.99 },
  "simple_status": "pending",
  "provisioning": {
    "overall": "pending",
    "lines": [
      { "index": 0, "type": "hosting", "state": "pending", "site_id": null, "domain": null },
      { "index": 1, "type": "domain",  "state": "pending", "site_id": null, "domain": null }
    ] } } }
The cart_item of an order line has a per-type shape: hosting = {"type":"hosting","product_uuid":"…","domain":"…","billing_period":"monthly|yearly"} (billing_period only needed for client_choice products; target_url required for hosting_forward); domain = {"type":"domain","action":"register|transfer","sld":"…","tld":"…","auth_code":"…"} (auth_code required for transfer); own space = {"type":"hosting_own","space_id":<int>,"domain":"…"} (price 0).

Flows

The main reseller flows, step by step.

Register a domain

  1. 1 Create a WHOIS contact (POST /contacts) or reuse an existing handle (GET /contacts).
  2. 2 Check availability and price (GET /domain-names/check and /price).
  3. 3 Register the domain (POST /domain-names) with domain and contact_handle.
  4. 4 Afterwards manage nameservers and DNS (PATCH /domain-names/{domain}, PUT /dns/zones/{domain}/records).

Order hosting and provision

  1. 1 Look up the product (GET /api/v1/products) and pick the package product_uuid.
  2. 2 Place the order (POST /api/v1/orders) with a hosting cart_item.
  3. 3 Poll GET /api/v1/orders/{id} until simple_status is completed or failed (no webhooks).
  4. 4 Read the hosting line site_id from provisioning.lines.
  5. 5 Use the site: GET /sites/{id} for usage, GET /sites/{id}/login for SSO.

Manage DNS (full replace)

  1. 1 Read the current set (GET /dns/zones/{domain}/records). 404 means no zone yet; treat it as empty.
  2. 2 Build the complete desired record set (managed types only; NS is preserved automatically).
  3. 3 Replace everything with PUT /dns/zones/{domain}/records and a {"records":[…]} body (an empty array clears it).
  4. 4 Check synced in the response (informational; stays false until the domain delegates to our nameservers).

Asynchronous provisioning

Provisioning is asynchronous and there are no webhooks. Poll GET /api/v1/orders/{id} until simple_status is completed or failed, then read the site via the line site_id. In production a queue worker delivers the orders.

Deliver-first billing

The restricted order path sets auto_deliver=true and send_invoice=false: the service is delivered first and billed afterwards through a subscription (needs_subscription and needs_payment in the response point to that follow-up). You pay your account price including your valuepack; you set your own retail price.

Caveats

Login is owner-scoped

SSO only logs in to your own or shared sites.

Unknown TLD returns "unknown"

For a TLD we do not sell, /check returns unknown and /price returns null. Treat that as not sellable.

Deliver-first subscription billing

Orders are delivered without sending an invoice or quote; billing then runs through a subscription.

Prices include your valuepack

All prices from the API are the price your account pays us, including your valuepack (excl. VAT). On top of that, as a reseller you set the retail price for your client yourself.

Reference implementation

Hosting panel boilerplate

A complete example integration against this API: domains, DNS, ordering and provisioning hosting.

GitHub
Stay up to date with recent developments! Subscribe and receive our newsletter Signing up... Thank you for subscribing! Something went wrong. Please try again later.