# API documentation

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. Register and transfer domains, manage DNS, order and provision hosting and manage client sites, exactly like the reference boilerplate does.

- Base URL: `https://ljpc-hosting.nl`
- Hosting paths live under `/api/hosting/v1`, 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

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

```bash
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 at support@ljpc.nl.

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

```json
// 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

### Domains and TLDs

#### 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`. Permission: none (valid key only).

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

```json
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`. Permission: Check domain availability.

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

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

```json
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. Permission: Check domain price.

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

```json
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. 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. |

```json
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. Permission: View domains and sites.

```json
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. 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. |

```bash
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
```

```json
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. 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. |

```bash
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
```

```json
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. 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. |

```bash
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
```

```json
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. Permission: View auth codes.

```json
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. Permission: View WHOIS contacts through the API.

```json
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. Permission: View WHOIS contacts through the API.

```json
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. 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. |

```bash
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
```

```json
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. Permission: Update WHOIS contacts through the API.

```bash
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
```

```json
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). Permission: Manage DNS through the API.

```json
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. 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. |

```bash
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
```

```json
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. Permission: Show own sites through the API.

```json
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` x 1024^3), `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). Permission: Show own sites through the API.

```json
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. Permission: Login to sites.

```json
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`. Permission: none (public).

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

```json
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`. 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 shapes below. |

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

```bash
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
```

```json
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. Permission: View own orders.

```json
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 }
    ] } } }
```

## Flows

### Register a domain

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

### Order hosting and provision

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

### Manage DNS (full replace)

1. Read the current set (`GET /dns/zones/{domain}/records`). 404 means no zone yet; treat it as empty.
2. Build the complete desired record set (managed types only; NS is preserved automatically).
3. Replace everything with `PUT /dns/zones/{domain}/records` and a `{"records":[…]}` body (an empty array clears it).
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

A complete example integration against this API (domains, DNS, ordering and provisioning hosting) lives in the hosting panel boilerplate: https://github.com/LJPc-solutions/HostingPanelBoilerplate


---

## Contact

- LJPc hosting, Dr. A.F. Philipsweg 15F, 9403 AC Assen, Netherlands
- Phone: +31 85 130 6429
- Email: support@ljpc.nl
- This page: https://ljpc-hosting.nl/en/api-documentation
