Skip to main content
Someone checks out, core makes a Service row, and your module makes the real thing exist.
Read Fundamentals first if you haven’t.

Generate it

Search for TODO and you’ll find the four or five places that need your API calls.

What core calls, and when

All of it is resolved by class name. Get a name wrong and it silently does nothing, which is what module:doctor is for.

Provisioning

The one class you can’t do without. It runs in a queued job after the payment clears.
app/Services/ModuleServiceActions.php
Throw on failure. You’re inside a queued job, so an exception gets you a retry. Returning a failure array doesn’t, and the customer has already been charged. This is the single most expensive mistake you can make in a service module.
Also implement the idempotency guard, which the generator gives you:
Core checks this before provisioning. Without it, a retried job or a webhook race creates a second server the customer never asked for.

Suspend, unsuspend, terminate

Three static methods, each taking a service ID rather than a model.
app/Services/Actions/SuspendServerService.php
That 404 handling matters more than it looks. Without it, terminating a service whose server someone deleted by hand gets stuck forever and an admin can never clear it off the books.

Renewal

BaseRenewService already owns the billing side: billing cycles, configurable option pricing, due dates, free trial conversion, admin suspension checks. You only handle the side effect.
app/Services/RenewService.php
For most modules that’s genuinely the whole file.

Cancellation

BaseCancelService handles the awkward parts. You supply two methods.
app/Services/CancelService.php
If there’s no server ID and the service is pending, it’s marked terminated and the cancellation succeeds. Provisioning failed, so there’s nothing to delete.Without this a customer can’t cancel a service that never worked.
A missing ID on an active service is a data problem somebody needs to look at, not something to quietly terminate.
So it can be retried, rather than ending up terminated locally with a live server.
It reads the raw attribute, because $user->password can be cast or hidden.

Checkout fields

Optional. Delete the class if your product needs no customer input.
app/Services/MarketCheckoutConfig.php
validateCheckoutData() has to return exactly ['validated' => [...], 'errors' => [...]], because core reads both keys by name. The generator writes a working version.

Customer panel

Optional.
Two halves that have to line up.
The path after service/{serviceId} has to match the tab href exactly, and your permission keys have to match those paths. That’s how the permission filter and the tab list stay in sync.
Icons are inline SVG strings (Lucide markup), not component names.
This is only called when the service is active, and you must accept both arguments ($serviceConfig, $serviceId) even if you ignore them. Core always passes them.

Upgrades, downgrades, addons

Optional. All three follow the same pattern.
Core has already taken the money by the time you’re called.
Return true and core renames the service, marks the invoice processed and notifies the customer. Return false and none of that happens, so the customer keeps their old resources. That’s the right failure mode. processDowngrade() is identical. AddonService has applyAddon() and removeAddon(), both taking (Service $service, array $addon, int $quantity = 1).

Test it properly

Then walk it:
1

Buy it

Create a product bound to your module and buy it as a test user.
2

Check the remote side

The server exists and Service.config has its ID.
3

Run the lifecycle

Suspend, unsuspend, renew, cancel from admin. Check the remote state each time.
4

Re-run provisioning

Run the job again for the same service. Confirm you don’t get a second server.
5

Break it on purpose

Wreck the credentials. Confirm the failure is loud, retried and visible to an admin.
6

Delete the server behind DezerX's back

Then terminate the service. It should still work.
Steps 4 to 6 are the ones that catch real bugs.

Worth reading

CPanelService

Clearest small implementation.

LicenseService

No external panel at all.

VirtfusionService

VPS lifecycle with addons.

PterodactylService

The big one: per-hour billing, auto-stock, file manager.