Colocation operations automation
Tickets, site access, shipments and facility data handled from code, with a morning report and an incident workflow.
Scenario#
An operations team runs cages in four IBXs. Engineers spend time in the Customer Portal web interface on work that follows a pattern: checking open tickets, scheduling a site visit with a shipment for a hardware swap, and opening a ticket when a link drops. The goal is to start these tasks from the change-management system.
Flow#
Safety rules for this domain#
The Customer Portal order APIs define no dry run. A work visit, a shipment order, a Smart Hands request or a ticket takes effect when the request is sent, and a Smart Hands request is billed.
- Keep reads and writes in separate code paths with separate credentials where possible. The morning report needs read entitlements only.
- Put a human approval in front of every order, in your change-management system.
- The SDK does not retry a
POSTcreate after a 5xx or a timeout, because the order may have been placed. After such a failure, check order history before sending the order again. - Put your change number in
customerReferenceId, which makes a duplicate order visible in order history.
Part 1: morning report#
The report uses one session, reads from three domains and writes nothing.
import com.eqixiac.equinix.Equinix;
import com.eqixiac.equinix.customerportal.enums.OrderHistoryProductType;
import com.eqixiac.equinix.customerportal.enums.OrderHistoryStatus;
import com.eqixiac.equinix.customerportal.model.OrderHistoryItem;
import com.eqixiac.equinix.customerportal.model.json.creators.OrderHistorySearchRequest;
import com.eqixiac.equinix.ibxsmartview.model.PowerEvent;
List<String> ibxs = List.of("DC6", "DC11", "SV5", "FR5");
try (Equinix eq = new Equinix(credentials)) {
// Open trouble tickets
OrderHistorySearchRequest openTickets = OrderHistorySearchRequest.builder()
.filters(OrderHistorySearchRequest.Filters.builder()
.productTypes(OrderHistoryProductType.TROUBLE)
.orderStatus(List.of(OrderHistoryStatus.SUBMITTED, OrderHistoryStatus.IN_PROGRESS))
.ibxs(ibxs)
.build())
.page(new OrderHistorySearchRequest.PageRequest(0, 100))
.build();
for (OrderHistoryItem ticket : eq.customerPortal().orderHistory().search(openTickets)) {
System.out.println("TICKET " + ticket.getOrderNumber() + " " + ticket.getOrderStatus()
+ " " + ticket.getIbx() + " since " + ticket.getCreatedAt());
}
// Power events in the facilities
for (PowerEvent event : eq.ibxSmartView().powerEvents().search(ibxs, null, null, 0, 50)) {
System.out.println("POWER " + event.getEventType() + " " + event.getStatus() + " " + event.getConditionType());
}
// Fabric ports that are not up
eq.fabric().ports().list().stream()
.filter(p -> !"ACTIVE".equals(String.valueOf(p.getState())))
.forEach(p -> System.out.println("PORT " + p.getName() + " " + p.getState()));
}The same three reads are available to an agent as the MCP tools portal_list_open_tickets and ibx_list_power_events, see MCP server.
Part 2: hardware swap#
Replacing a failed line card requires an inbound shipment for the new card, a work visit for the engineer and an outbound shipment for the old card. The three orders carry the same change number.
Site access#
import com.eqixiac.equinix.customerportal.model.OrderResponse;
import com.eqixiac.equinix.customerportal.model.json.creators.WorkVisitCage;
import com.eqixiac.equinix.customerportal.model.json.creators.WorkVisitDetails;
import com.eqixiac.equinix.customerportal.model.json.creators.WorkVisitOrderRequest;
import com.eqixiac.equinix.customerportal.model.json.creators.WorkVisitVisitor;
String change = "CHG-10492";
// Valid cage identifiers for this user
portal.workVisits().listLocations().forEach(System.out::println);
OrderResponse visit = portal.workVisits().order(
WorkVisitOrderRequest.builder(
WorkVisitDetails.builder(
List.of(new WorkVisitCage("DC6:01:012345").accountNumber("123456")),
"2026-11-20T14:00:00Z",
"2026-11-20T18:00:00Z",
List.of(WorkVisitVisitor.nonRegistered("Dana", "Okafor", "Example Networks")))
.openCabinet(true)
.build())
.description("Replace line card, router dc6-core-1, slot 3")
.customerReferenceId(change)
.build());
System.out.println("work visit ordered: " + visit);Shipments and hands#
portal.shipments().orderInbound(...) announces the replacement part so that the IBX receives it, with the carrier, the tracking numbers and whether to deliver it to the cage. orderOutbound(...) arranges the return. If nobody travels, a Smart Hands equipment-install order has Equinix staff fit the card: portal.smartHandsRequests().createEquipmentInstall(...). Each request type has its own details class with the fields that order kind needs. Build them from the Javadoc of customerportal.model.json.creators, and use listTypes() and listLocations() for the valid kinds and locations.
Confirmation#
OrderHistorySearchRequest byChange = OrderHistorySearchRequest.builder()
.filters(OrderHistorySearchRequest.Filters.builder().ibxs(List.of("DC6")).build())
.page(new OrderHistorySearchRequest.PageRequest(0, 50))
.build();
portal.orderHistory().search(byChange).stream()
.filter(o -> o.getCustomerReferenceNumbers() != null && o.getCustomerReferenceNumbers().contains(change))
.forEach(o -> System.out.println(o.getOrderNumber() + " " + o.getType() + " " + o.getOrderStatus()));Order history should contain three orders with the change number. Fewer than three means that a create failed, and more than three means that an order was sent twice.
Part 3: incident workflow#
A monitoring alert reports loss of signal on a cross connect. Before it opens a ticket, the automation collects the facility events and the Fabric events for the port.
import com.eqixiac.equinix.customerportal.enums.TicketCode;
import com.eqixiac.equinix.customerportal.model.json.creators.TicketNoteRequest;
import com.eqixiac.equinix.customerportal.model.json.creators.TroubleTicketCreateRequest;
import com.eqixiac.equinix.fabric.model.CloudEvent;
import com.eqixiac.equinix.fabric.model.Port;
import com.eqixiac.equinix.fabric.model.implementation.filter.CloudEventQuery;
Port port = fabric.ports().getByUuid(portUuid);
// 1. Is the facility reporting anything?
boolean facilityEvent = smartView.powerEvents().search(List.of("DC6"), null, null, 0, 10).iterator().hasNext();
// 2. What did Fabric record for this port?
StringBuilder evidence = new StringBuilder();
for (CloudEvent event : fabric.cloudEvents().search(
CloudEventQuery.forAsset("ports", portUuid).after(LocalDateTime.now(ZoneOffset.UTC).minusHours(2)))) {
evidence.append(event.getTime()).append(' ').append(event.getType()).append('\n');
}
// 3. Open the ticket with the evidence
String ticketId = portal.troubleTickets().create(
TroubleTicketCreateRequest.builder(
TicketCode.CODE_0001_0000, // choose the code for the problem category
"Loss of signal on Fabric port " + port.getName() + " in " + port.getLocation().getMetroCode()
+ ". Facility power event active: " + facilityEvent,
Instant.now().toString(),
"DC6:01:012345")
.customerReferenceId("INC-48211")
.build());
portal.troubleTickets().addNote(ticketId, new TicketNoteRequest("Fabric events, last 2 h:\n" + evidence));The workflow should first check order history for an open ticket on the same asset, and add a note to it instead of opening a second one.
Part 4: monthly billing check#
import com.eqixiac.equinix.customerportal.model.InvoiceSummary;
Map<String, BigDecimal> recurring = new TreeMap<>();
Map<String, BigDecimal> nonRecurring = new TreeMap<>();
for (InvoiceSummary invoice : portal.invoices().summaries().loadAll()) {
String currency = invoice.getCurrencyCode().getCurrencyCode();
recurring.merge(currency, invoice.getTotalRecurringAmount(), BigDecimal::add);
nonRecurring.merge(currency, invoice.getTotalNonRecurringAmount(), BigDecimal::add);
}
System.out.println("recurring " + recurring + ", non-recurring " + nonRecurring);Smart Hands orders appear as non-recurring charges, so a rise in that total can be compared with the Smart Hands orders in order history for the same period.
Service behaviour to plan for#
- Some Customer Portal endpoints answered 5xx intermittently on 2026-11-11. The SDK retries reads. After a failed create, the order may or may not exist until order history has been checked.
- The v1 SmartView calls and the asset calls need the account number.
- The order APIs return an order number and no resource, so the status of every order is read from order history.