# Let customers manage IPs, protection and domains

> The portal API for a customer's Floating IPs, DDoS Shield, external servers and platform domain names.

Source: https://www.coritan.com/docs/organizations/storefront/portal-networking/

These portal routes let a customer order and attach [Floating IPs](/docs/floating-ips/), filter the traffic that reaches them with [DDoS Shield](/docs/ddos-shield/), point an external server at a game server they host elsewhere, and claim a free name under a domain your brand lends. Every route takes the customer's token as `Authorization: Bearer $CUSTOMER_TOKEN` and works only on services that customer bought. Sharing a server with another customer does not share its addresses.

## Before you begin

- Get a customer token as [Sign customers in to your storefront](/docs/organizations/storefront/customer-sign-in/) describes.
- Sell the plans in your catalogue: an IP product for addresses and an external server product for external servers. [Set up products and pricing](/docs/organizations/products-and-pricing/) explains products and plans.
- Use the right id. The IP and DDoS Shield routes take the address's `service_id` from `GET /portal/ips`. The external server routes take the service `id` from `GET /portal/external-servers`, which is the id `GET /portal/services` returns too.

## Order a floating IP

1. List the pools that sell what the customer wants:

   ```bash
   curl "https://api.coritan.com/api/v1/orgs/acme/portal/ips/pools?version=ipv4&prefix_len=32" \
     -H "Authorization: Bearer $CUSTOMER_TOKEN"
   ```

   Each pool in `pools` has its `id`, `name`, `region`, `country_code` and `geo_label`, and `available` says whether it has an address to sell. `version` is `ipv4` or `ipv6`. `prefix_len` is `32` for one address, or 24–29 for a subnet; the list is empty for subnets while we do not rent them. `region` narrows the list to one location. [IP pools and regions](/docs/floating-ips/pools/) describes the pools.

2. Find the `id` of your IP product and of its plan, as [List products and plans](/docs/organizations/storefront/storefront-api/#list-products-and-plans) shows.

3. Create a key for this checkout in the browser, such as the value of `crypto.randomUUID()`, and place the order:

   ```bash
   curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/ips/order" \
     -H "Authorization: Bearer $CUSTOMER_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"org_product_id": 14, "org_pricing_id": 40, "pool_id": 3, "prefix_len": 32, "version": "ipv4", "idempotency_key": "5b0f7c1e-ip-order"}'
   ```

   | Field | What it holds |
   | --- | --- |
   | `org_product_id`, `org_pricing_id` | Your IP product and the plan. |
   | `pool_id` | The pool from step 1. The address comes from this pool only. |
   | `prefix_len`, `version` | The size and family. They default to `32` and `ipv4`. |
   | `idempotency_key` | Required. 1–64 letters, digits, dots, underscores, colons or hyphens. Keep it for the whole checkout, so a second click or a second tab returns the same order. |
   | `attach_to_org_service_id` | Optional. The service `id` of one of the customer's Container Apps servers that has no floating IP yet. We attach the address to it as soon as it is assigned. It works only when `prefix_len` is `32`. |
   | `hostname` | Optional. A name for the service. |

   The answer has the fields of a [storefront order](/docs/organizations/storefront/storefront-api/#take-an-order), such as `invoice_id`, `requires_payment` and `status`. It adds `intent`, which follows the order, and `hold_expires_at`.

4. When `requires_payment` is `true`, send the customer to pay, as [Pay an invoice](/docs/organizations/storefront/storefront-api/#pay-an-invoice) shows. We keep the address for the customer until `hold_expires_at`. An order that stays unpaid past then is cancelled, and the address goes back to the pool.

## Follow or cancel an order

| Route | What it does |
| --- | --- |
| `GET /portal/ips/orders` | The customer's open orders, so a checkout carries on after a reload or in another tab. Each has its `idempotency_key` and a `status` of `started`, `held`, `invoiced` or `provisioning`. |
| `GET /portal/ips/orders/{idempotency_key}` | One order, with its invoice's `invoice_number`, `total`, `currency`, `due_date` and `invoice_status`. |
| `POST /portal/ips/orders/{idempotency_key}/cancel` | Cancels an order whose address is not assigned yet, with its open invoice. |

Sending an order again with the same key returns the first order, with `replay: true`, once it has an invoice. A key whose order failed, was cancelled or expired starts a new order.

## Show a customer's addresses

`GET /portal/ips` returns `items` and `total`. Each item is one address or one subnet:

| Field | Meaning |
| --- | --- |
| `service_id` | The id the other IP and DDoS Shield routes take. |
| `kind` | `standalone` for one address, or `block` for a subnet. |
| `address`, `cidr`, `version` | The address, or the subnet's first address, with its prefix. |
| `service_status` | The service's status, such as `active` or `suspended`. |
| `pool_name`, `region`, `geo_label` | Where the address comes from. |
| `target_type`, `target_service_id`, `is_primary`, `attached_at` | What the address is attached to. They are empty when it is not attached. |
| `host_state`, `host_error` | On a Container Apps server, whether the address has reached the server: `pending`, `delivered`, `failed` or `removing`, and why it failed. |
| `ptr_hostname` | The reverse DNS name of a single address. |

Show prices and due dates from the customer's service in `GET /portal/services`, which holds what the customer pays you.

`GET /portal/ips/{service_id}` returns one item. A subnet also has `hosts_total`, `hosts_attached` and `hosts_available`. `GET /portal/ips/{service_id}/hosts` lists each address in it, with `attachable` and what it is attached to; add `include_reserved=false` to leave out the addresses nobody can attach. [How subnets work](/docs/floating-ips/subnets/) explains which addresses those are.

## Attach and detach an address

1. List what the address can go on:

   ```bash
   curl "https://api.coritan.com/api/v1/orgs/acme/portal/ips/attach-targets" \
     -H "Authorization: Bearer $CUSTOMER_TOKEN"
   ```

   Each item is one of the customer's Cloud Compute instances (`module_name` is `vps`) or Container Apps servers (`container`), with its `service_id`, `hostname` and `has_floating_ip`.

2. Attach the address:

   ```bash
   curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/ips/$IP_SERVICE_ID/attach" \
     -H "Authorization: Bearer $CUSTOMER_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"target_type": "vps", "target_service_id": 5120}'
   ```

   `target_type` is `vps` or `container`, and `target_service_id` is the target's `service_id` from step 1. For a subnet, `host_address` picks the address in it; leave it out to use the first free one. On an instance, the first address you attach becomes the primary one; send `is_primary: true` to make a later address primary, or `false` to keep it secondary. A server takes one floating IP.

3. To take it off, send `POST /portal/ips/{service_id}/detach`. For a subnet with more than one address attached, send the one to take off as `host_address`.

[Attach and detach a floating IP](/docs/floating-ips/attach-and-detach/) explains what attaching does on each kind of target.

## Set reverse DNS

```bash
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/portal/ips/$IP_SERVICE_ID/ptr" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostname": "mail.example.com"}'
```

`hostname` must be a full domain name, up to 253 characters. For an address in a subnet, add the address as `host_address`. `GET /portal/ips/{service_id}/ptr` reads the current name, and `hostname` is `null` when there is none. [Set reverse DNS for a floating IP](/docs/floating-ips/reverse-dns/) covers reverse DNS in more detail.

## Filter traffic with DDoS Shield

DDoS Shield filters traffic to every address with our standard profile. A customer can give an address a profile of their own and add rules to it.

| Route | What it does |
| --- | --- |
| `GET /portal/shield/status` | Each of the customer's addresses in `ips`, with its `ip_service_id`, `address`, whether it is `attached`, and its `protection`: `mode` is `platform_default` or `custom`, with the profile's `profile_name`. |
| `GET /portal/shield/events` | Attacks on the customer's addresses, with the ones still running first. Narrow them with `ip_service_id` and `limit` (default 50). |
| `GET /portal/shield/subjects/{ip_service_id}/profile` | The address's `mode`, `profile` and `rules`. `editable` is `false` on the standard profile, which the customer can read and cannot change. |
| `PATCH /portal/shield/subjects/{ip_service_id}/profile` | Gives the address a profile of the customer's own and returns it. Sending it again changes nothing. |
| `POST /portal/shield/subjects/{ip_service_id}/rules` | Adds a rule, and gives the address its own profile first when it has none. |
| `DELETE /portal/shield/subjects/{ip_service_id}/rules/{rule_id}` | Deletes a rule. |

Each event has the `address`, the `attack_type`, the `drop_reason`, its size as `pps`, `bps` and `unique_sources`, the `top_sources`, `started_at` and `ended_at`, and `active` while it is still running. [Read attack events](/docs/ddos-shield/attack-events/) explains what they mean.

A profile of the customer's own starts with our standard rate limits and no rules, and lets through traffic that no rule matches. Add a rule:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/shield/subjects/$IP_SERVICE_ID/rules" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Block a scanner", "action": "deny", "src_prefix": "203.0.113.0/24"}'
```

| Field | What it matches or does |
| --- | --- |
| `action` | `deny` (the default), `allow` or `rate_limit`. |
| `rate_limit_pps` | The packets per second a `rate_limit` rule lets through. |
| `protocol` | The IP protocol number, such as `6` for TCP, `17` for UDP or `1` for ICMP. |
| `src_prefix`, `dst_prefix` | A source or destination range in CIDR form, such as `203.0.113.0/24`. |
| `src_port_min`, `src_port_max`, `dst_port_min`, `dst_port_max` | Port ranges. A minimum with no maximum matches that one port. |
| `tcp_flags_mask`, `tcp_flags_value` | The TCP flags to test, and the value they must have. |
| `packet_len_min`, `packet_len_max`, `ttl_min`, `ttl_max` | Ranges of packet length and time to live. |
| `icmp_type`, `icmp_code` | The ICMP type and code. |
| `name`, `notes`, `enabled` | A label up to 191 characters, a note, and `false` to keep the rule without applying it. |

Check each value in your storefront before you send it: protocol, TCP flag, time to live and ICMP values are whole numbers from 0 to 255, ports and packet lengths from 0 to 65535, and `rate_limit_pps` is a positive number.

> [!WARNING]
> A field you leave out matches every packet, and `action` defaults to `deny`. A rule sent with no match fields blocks all traffic to the address.

[Change a floating IP's DDoS protection](/docs/floating-ips/shield-settings/) describes profiles and rules for Coritan customers.

## Run an external server

An external server gives a game server the customer hosts elsewhere a join address on your network. The customer orders it through [Take an order](/docs/organizations/storefront/storefront-api/#take-an-order), and these routes manage it once it exists. [How external servers work](/docs/proxies/external-servers/) explains the product.

`GET /portal/external-servers` lists the customer's external servers. Each has the `id` the routes take, `name`, `billing_status`, `join_address`, `mode`, `upstream` (the `host` and `port` players are sent to), `region`, and the last test in `probe`. `state` is `online`, `offline`, `suspended` or `unknown`. Until its first invoice is paid, an external server has no join address and its `state` is `unknown`. `GET /portal/external-servers/{id}` adds `edge_addresses` and `custom_domain`, and `GET /portal/external-servers/regions` lists the `regions` it can move to, with the `default_region_id`.

Change where players go:

```bash
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/portal/external-servers/$SERVICE_ID" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"upstream_host": "203.0.113.25", "upstream_port": 25565}'
```

| Field | What it changes |
| --- | --- |
| `upstream_host`, `upstream_port` | Where the server listens: a public IP address or a full hostname, and a port from 1 to 65535. |
| `mode` | `minecraft_java` or `minecraft_bedrock`. |
| `proxy_protocol` | `true` sends each player's address in a PROXY protocol header. Turn it on only when the server expects one. Bedrock servers cannot use it. |
| `real_ip` | With PROXY protocol off, adds the player's address to the hostname in the handshake. It is on by default. Turn it off for a proxy such as Velocity or BungeeCord that cannot accept the longer hostname. |
| `bedrock_bridge` | For a Java server that also runs Geyser, lets Bedrock players join at the same address. |
| `location_id` | The region players connect through, from the regions list. |
| `display_name` | The server's name, 1–100 characters. |

We refuse an address in a private or reserved range, an address of our own proxies, another customer's server on our network, and a server already connected to another external server. The answer is the external server as it now stands. After an address change we test the server at once, so `probe` shows whether it answers. `POST /portal/external-servers/{id}/test` tests it again and returns the `probe` with a `state` of `online` or `offline`. [Connect a server you host elsewhere](/docs/proxies/external-servers/connect-an-external-server/) covers the connection itself.

Change what players see in their server list with `PATCH /portal/services/{service_id}/gameproxy`, as [Show the customer's services](/docs/organizations/storefront/customer-portal/#show-the-customer-s-services) describes, and list the server in your directory with `PUT /portal/services/{service_id}/community-listing`, as [Run a community with a server list, forum and guides](/docs/organizations/storefront/community/) describes.

## Link a domain to an external server

The routes under `/portal/external-servers/{id}/custom-domain` work as [Link a custom domain](/docs/organizations/storefront/portal-servers/#link-a-custom-domain) describes for a Container Apps server. `GET` shows the link and the `expected_cname_target`, `POST` links a `domain` and `subdomain`, `POST /verify` checks the record now, `PATCH` changes `include_in_list`, and `DELETE` unlinks the name.

When the name is under a free name the customer claimed, as the next section describes, we write the CNAME record for them and the link is verified at once. Use a subdomain such as `play` there, since a CNAME record cannot sit at the root of a zone we host.

## Offer free names under your domain

When your brand lends a domain, such as `example.com`, each customer can claim a name under it, such as `survival.example.com`, and use it for their servers. We set up the domains your brand lends; ask [support](https://www.coritan.com/dashboard/support) to add one. The names resolve only when that domain's zone is on Coritan's nameservers, since we delegate each name from it.

1. Read the overview:

   ```bash
   curl "https://api.coritan.com/api/v1/orgs/acme/portal/platform-domains" \
     -H "Authorization: Bearer $CUSTOMER_TOKEN"
   ```

   `suffixes` lists the domains on offer, and `claims` the customer's names. `limit` is how many names a customer may hold, and `remaining` how many more they can claim; `remaining` is `null` when there is no limit. `rules` has the `min_length`, `max_length` and `pattern` a name must fit, and the `reserved` names nobody can claim. Once the customer has a name, `suggestions` proposes a hostname under it for each product, such as `play.survival.example.com` for a game server.

2. Check a name while the customer types it:

   ```bash
   curl "https://api.coritan.com/api/v1/orgs/acme/portal/platform-domains/availability?label=survival&suffix=example.com" \
     -H "Authorization: Bearer $CUSTOMER_TOKEN"
   ```

   The answer has `available`, the full name as `fqdn`, and the `reason` when it is not available. `suffix` defaults to the first domain on offer.

3. Claim it:

   ```bash
   curl -X POST "https://api.coritan.com/api/v1/orgs/acme/portal/platform-domains/claims" \
     -H "Authorization: Bearer $CUSTOMER_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"label": "survival", "suffix": "example.com"}'
   ```

   The answer, with status `201`, has the `fqdn` and `zone_id`. `parent_delegated` says whether we delegated the name from your domain's zone, `dnssec` whether we signed it, and `certificate` whether we requested a wildcard certificate for it, with the `reason` when we did not.

Each name is a DNS zone that we sign with DNSSEC. It appears on your organization's **DNS** tab, where your team can add and change its records, as [Host DNS zones for your organization](/docs/organizations/dns/) describes. The portal has no routes for customers to edit records.

`DELETE /portal/platform-domains/claims/{claim_id}`, with the claim's `id` from `claims`, releases a name and returns its `fqdn`.

> [!WARNING]
> Releasing a name deletes its zone with every record in it, and we stop renewing its certificates. Hostnames under it stop resolving, so move anything that uses them first.

## Result

Your storefront lets each customer order, attach and protect their addresses, run their external servers and claim names, and every route checks that the service belongs to the customer who sent it. Claims, releases and changes to external servers appear in your [organization audit log](/docs/organizations/audit-log/) with the customer as the actor.

## Troubleshooting

`Unsuspend the IP service before attaching`
: The address's service is suspended. Unsuspend it as [Suspend or unsuspend a service](/docs/organizations/customer-services/#suspend-or-unsuspend-a-service) describes.

`Container already has a primary floating IP; detach first`
: A server takes one floating IP. Detach the other address first.

`Subnet has multiple attached hosts; pass host_address to detach one`
: Send the address to take off as `host_address`.

`host_address / host_inventory_id only apply to subnet block services`
: The address is a single one. Leave out `host_address`.

`No free hosts available in this subnet`
: Every address in the subnet is attached. Detach one first.

`An identical IP order is already in progress; retry in a moment`
: The first request with this key is still running. Retry with the same key.

`This IP order can no longer be cancelled`
: The order is past payment, or has already ended. To give up an address the customer holds, cancel its service as [Cancel a service](/docs/organizations/storefront/customer-portal/#cancel-a-service) shows.

`Only a single IPv4 can be attached to a server from its Network tab`
: `attach_to_org_service_id` works only when `prefix_len` is `32`. Order a subnet without it.

`PTR hostname must be a fully-qualified domain name`
: Send a full name, such as `mail.example.com`.

`403` with `Rule does not belong to this subject profile`
: The rule is on another address's profile. Delete it through that address.

`409` with `This external server is not connected yet; pay its invoice first`
: The external server has no join address until its first invoice is paid.

`409` with `This external server is suspended`
: A suspended external server cannot be changed. Unsuspend its service first.

`422` with `errors`
: We refused the new address or setting, and `errors` says why, such as an address in a private range or PROXY protocol on a Bedrock server.

`A CNAME can't sit at the root of a zone hosted here. Use a subdomain such as play.`
: The domain is one of the customer's free names. Link a subdomain of it.

`404` with `This brand does not offer platform names`
: Your brand lends no domain yet. Ask support to add one.

`409` with `Already taken`
: Another customer holds the name, or a game server's join address uses it. Choose another.

`429` with `rate_limited`
: The customer repeated an action too quickly. Wait, then try again.

## Related

- [Build a storefront on the Organization API](/docs/organizations/storefront/)
- [Floating IPs](/docs/floating-ips/)
- [DDoS Shield](/docs/ddos-shield/)
- [How external servers work](/docs/proxies/external-servers/)
- [Host DNS zones for your organization](/docs/organizations/dns/)

## API

- `GET /api/v1/orgs/{org_slug}/portal/ips`: List my IPs (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-get-api-v1-orgs-org-slug-portal-ips)
- `GET /api/v1/orgs/{org_slug}/portal/ips/attach-targets`: List attach targets (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-get-api-v1-orgs-org-slug-portal-ips-attach-targets)
- `POST /api/v1/orgs/{org_slug}/portal/ips/order`: Order a floating IP / subnet via the org storefront path (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-post-api-v1-orgs-org-slug-portal-ips-order)
- `GET /api/v1/orgs/{org_slug}/portal/ips/orders`: Open IP order intents, so unpaid or in-flight orders resume after login or in a new tab (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-get-api-v1-orgs-org-slug-portal-ips-orders)
- `GET /api/v1/orgs/{org_slug}/portal/ips/orders/{idempotency_key}`: Get my IP order (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-get-api-v1-orgs-org-slug-portal-ips-orders-idempotency-key)
- `POST /api/v1/orgs/{org_slug}/portal/ips/orders/{idempotency_key}/cancel`: Cancel my IP order (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-post-api-v1-orgs-org-slug-portal-ips-orders-idempotency-key-cancel)
- `GET /api/v1/orgs/{org_slug}/portal/ips/pools`: List sellable pools (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-get-api-v1-orgs-org-slug-portal-ips-pools)
- `GET /api/v1/orgs/{org_slug}/portal/ips/{service_id}`: Get my IP (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-get-api-v1-orgs-org-slug-portal-ips-service-id)
- `POST /api/v1/orgs/{org_slug}/portal/ips/{service_id}/attach`: Attach IP (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-post-api-v1-orgs-org-slug-portal-ips-service-id-attach)
- `POST /api/v1/orgs/{org_slug}/portal/ips/{service_id}/detach`: Detach IP (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-post-api-v1-orgs-org-slug-portal-ips-service-id-detach)
- `GET /api/v1/orgs/{org_slug}/portal/ips/{service_id}/hosts`: List my IP hosts (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-get-api-v1-orgs-org-slug-portal-ips-service-id-hosts)
- `GET /api/v1/orgs/{org_slug}/portal/ips/{service_id}/ptr`: Get PTR (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-get-api-v1-orgs-org-slug-portal-ips-service-id-ptr)
- `PATCH /api/v1/orgs/{org_slug}/portal/ips/{service_id}/ptr`: Set PTR (https://www.coritan.com/docs/api/reference/organizations/customer-portal/ips/#op-patch-api-v1-orgs-org-slug-portal-ips-service-id-ptr)
- `GET /api/v1/orgs/{org_slug}/portal/shield/events`: Shield events list (https://www.coritan.com/docs/api/reference/organizations/customer-portal/shield/#op-get-api-v1-orgs-org-slug-portal-shield-events)
- `GET /api/v1/orgs/{org_slug}/portal/shield/status`: Shield status (https://www.coritan.com/docs/api/reference/organizations/customer-portal/shield/#op-get-api-v1-orgs-org-slug-portal-shield-status)
- `GET /api/v1/orgs/{org_slug}/portal/shield/subjects/{ip_service_id}/profile`: Get subject profile (https://www.coritan.com/docs/api/reference/organizations/customer-portal/shield/#op-get-api-v1-orgs-org-slug-portal-shield-subjects-ip-service-id-profile)
- `PATCH /api/v1/orgs/{org_slug}/portal/shield/subjects/{ip_service_id}/profile`: Promote subject to a customer-editable custom profile (idempotent) (https://www.coritan.com/docs/api/reference/organizations/customer-portal/shield/#op-patch-api-v1-orgs-org-slug-portal-shield-subjects-ip-service-id-profile)
- `POST /api/v1/orgs/{org_slug}/portal/shield/subjects/{ip_service_id}/rules`: Create subject rule (https://www.coritan.com/docs/api/reference/organizations/customer-portal/shield/#op-post-api-v1-orgs-org-slug-portal-shield-subjects-ip-service-id-rules)
- `DELETE /api/v1/orgs/{org_slug}/portal/shield/subjects/{ip_service_id}/rules/{rule_id}`: Delete subject rule (https://www.coritan.com/docs/api/reference/organizations/customer-portal/shield/#op-delete-api-v1-orgs-org-slug-portal-shield-subjects-ip-service-id-rules-rule-id)
- `GET /api/v1/orgs/{org_slug}/portal/external-servers`: List external servers (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers/#op-get-api-v1-orgs-org-slug-portal-external-servers)
- `GET /api/v1/orgs/{org_slug}/portal/external-servers/regions`: Regions an existing external server can be moved to (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers/#op-get-api-v1-orgs-org-slug-portal-external-servers-regions)
- `GET /api/v1/orgs/{org_slug}/portal/external-servers/{service_id}`: Get external server (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers/#op-get-api-v1-orgs-org-slug-portal-external-servers-service-id)
- `PATCH /api/v1/orgs/{org_slug}/portal/external-servers/{service_id}`: Change where players are forwarded, how, and through which region (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers/#op-patch-api-v1-orgs-org-slug-portal-external-servers-service-id)
- `GET /api/v1/orgs/{org_slug}/portal/external-servers/{service_id}/custom-domain`: Get custom domain (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers-custom-domain/#op-get-api-v1-orgs-org-slug-portal-external-servers-service-id-custom-domain)
- `POST /api/v1/orgs/{org_slug}/portal/external-servers/{service_id}/custom-domain`: Link custom domain (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers-custom-domain/#op-post-api-v1-orgs-org-slug-portal-external-servers-service-id-custom-domain)
- `PATCH /api/v1/orgs/{org_slug}/portal/external-servers/{service_id}/custom-domain`: Show the vanity hostname, or the hosted one, in the public server list (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers-custom-domain/#op-patch-api-v1-orgs-org-slug-portal-external-servers-service-id-custom-domain)
- `DELETE /api/v1/orgs/{org_slug}/portal/external-servers/{service_id}/custom-domain`: Unlink custom domain (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers-custom-domain/#op-delete-api-v1-orgs-org-slug-portal-external-servers-service-id-custom-domain)
- `POST /api/v1/orgs/{org_slug}/portal/external-servers/{service_id}/custom-domain/verify`: Check the CNAME now rather than waiting for the next maintenance pass (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers-custom-domain/#op-post-api-v1-orgs-org-slug-portal-external-servers-service-id-custom-domain-verif)
- `POST /api/v1/orgs/{org_slug}/portal/external-servers/{service_id}/test`: Status-ping the backend now and record the answer (https://www.coritan.com/docs/api/reference/organizations/customer-portal/external-servers/#op-post-api-v1-orgs-org-slug-portal-external-servers-service-id-test)
- `GET /api/v1/orgs/{org_slug}/portal/platform-domains`: Overview (https://www.coritan.com/docs/api/reference/organizations/customer-portal/platform-domains/#op-get-api-v1-orgs-org-slug-portal-platform-domains)
- `GET /api/v1/orgs/{org_slug}/portal/platform-domains/availability`: Check availability (https://www.coritan.com/docs/api/reference/organizations/customer-portal/platform-domains/#op-get-api-v1-orgs-org-slug-portal-platform-domains-availability)
- `POST /api/v1/orgs/{org_slug}/portal/platform-domains/claims`: Create claim (https://www.coritan.com/docs/api/reference/organizations/customer-portal/platform-domains/#op-post-api-v1-orgs-org-slug-portal-platform-domains-claims)
- `DELETE /api/v1/orgs/{org_slug}/portal/platform-domains/claims/{claim_id}`: Delete claim (https://www.coritan.com/docs/api/reference/organizations/customer-portal/platform-domains/#op-delete-api-v1-orgs-org-slug-portal-platform-domains-claims-claim-id)
