---
title: Delete a service
description: Retire-or-delete.
api_method: DELETE
api_path: "/v1/services/{serviceId}"
canonical_url: https://wiblo.app/docs/developers/api/services/delete-service
last_updated: 2026-07-28T17:31:44+02:00
md_url: https://wiblo.app/docs/developers/api/services/delete-service.md
---

# Delete a service

`DELETE /v1/services/{serviceId}`

Retire-or-delete. A service with live (pending/accepted) bookings soft-retires: it leaves default lists and the public surface while its bookings keep their service edge and stay reschedulable. Only a service with no live bookings hard-deletes (host rows cascade). Both outcomes answer 200 with `outcome` naming the branch; `service` carries the surviving retired row (WIBLO-143 — the admin UI renders the "retired instead" state from it) and is null after a hard delete. Owner/admin only. Per-user rate-limited at 60/60s.

## Path parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `serviceId` | `string` | Yes |  |

## Headers

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `X-Wiblo-Workspace` | `string` | Yes | Workspace id the call is scoped to. The actor must hold an active membership in it; a foreign or unknown id collapses onto 404 WORKSPACE_NOT_FOUND. |

## Request

**curl**

```bash
curl https://api.wiblo.app/v1/services/3f8a2b9c-51d4-4e0b-9c6a-7d2e8f1a4b5c \
  -X DELETE \
  -H "Authorization: Bearer $WIBLO_TOKEN" \
  -H "X-Wiblo-Workspace: $WIBLO_WORKSPACE_ID"
```

**TypeScript**

```ts
import { createSdk, deleteService } from "@workspace/sdk"

const sdk = createSdk({ baseUrl: "https://api.wiblo.app" })

const { data, error } = await deleteService({
  client: sdk,
  path: { serviceId: "3f8a2b9c-51d4-4e0b-9c6a-7d2e8f1a4b5c" },
  headers: { "X-Wiblo-Workspace": "..." },
})
```

## Example response

```json
{
  "outcome": "retired",
  "service": {
    "id": "3f8a2b9c-51d4-4e0b-9c6a-7d2e8f1a4b5c",
    "workspace_id": "9c1b7e24-6a3f-4d58-b2e9-0f4a8c6d1e37",
    "title": "Initial consultation",
    "slug": "initial-consultation",
    "description": "A 30-minute intro call to scope your project and answer questions.",
    "category": "Consultations",
    "hidden": false,
    "position": 1,
    "length": 30,
    "time_zone": null,
    "slot_interval": 30,
    "offset_start": 0,
    "price": 7500,
    "currency": "usd",
    "scheduling_type": "round_robin",
    "assign_all_team_members": false,
    "is_rr_weights_enabled": false,
    "reschedule_with_same_rr_host": true,
    "minimum_booking_notice": 120,
    "before_event_buffer": 10,
    "after_event_buffer": 10,
    "booking_limits": {
      "day": 4,
      "week": 20
    },
    "duration_limits": null,
    "max_active_bookings_per_booker": 3,
    "period_type": "rolling",
    "period_days": 60,
    "period_start_date": null,
    "period_end_date": null,
    "locations": [
      {
        "type": "video",
        "link": "https://meet.example.com/acme"
      }
    ],
    "recurring_event": null,
    "requires_confirmation": false,
    "requires_booker_email_verification": false,
    "disable_guests": false,
    "hide_calendar_notes": false,
    "hide_calendar_event_details": false,
    "hide_organizer_email": false,
    "lock_time_zone": false,
    "disable_cancelling": false,
    "disable_rescheduling": false,
    "seats_per_time_slot": null,
    "seats_show_attendees": false,
    "seats_show_availability_count": true,
    "color": {
      "light": "#0ea5e9",
      "dark": "#38bdf8"
    },
    "booking_fields": null,
    "metadata": null,
    "retired_at": "2026-07-28T09:12:00.000Z",
    "created_at": "2026-06-12T09:30:00.000Z",
    "updated_at": "2026-07-28T09:12:00.000Z",
    "tags": [
      {
        "id": "d4c8b1a2-3e5f-4a6b-8c9d-0e1f2a3b4c5d",
        "name": "Intro",
        "color": "#38bdf8"
      }
    ]
  }
}
```

## Responses

**`200`** — The delete resolved — `outcome` says whether the service was retired or hard-deleted. Returns `DeleteServiceResponse`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `outcome` | `"retired" \| "deleted"` | Yes | `retired` when live bookings forced a soft retire; `deleted` after a hard delete. |
| `service` | `object \| null` | Yes | The surviving retired row; `null` after a hard delete. |
| `service.id` | `string` | Yes | Unique id of the service. |
| `service.workspace_id` | `string` | Yes | Workspace the service belongs to. |
| `service.title` | `string` | Yes | Display name of the service. |
| `service.slug` | `string` | Yes | URL-safe identifier, unique within the workspace. |
| `service.description` | `string \| null` | Yes | Long-form description shown on the booking page. |
| `service.category` | `string \| null` | Yes | Free-form grouping label for the admin catalogue. |
| `service.hidden` | `boolean` | Yes | Hidden services never appear on public booking surfaces. |
| `service.position` | `integer` | Yes | Manual sort position; lists order by it, then creation time. |
| `service.length` | `integer` | Yes | Appointment length in minutes. |
| `service.time_zone` | `string \| null` | Yes | IANA time zone the service is anchored to; `null` follows the visitor. |
| `service.slot_interval` | `integer \| null` | Yes | Minutes between offered start times; `null` falls back to the service length. |
| `service.offset_start` | `integer` | Yes | Minutes past each interval boundary that slots start at. |
| `service.price` | `integer` | Yes | Price of the service; `0` means free. |
| `service.currency` | `string` | Yes | Lowercase 3-letter ISO 4217 currency code. |
| `service.scheduling_type` | `"round_robin" \| "collective" \| null` | Yes | Team scheduling mode — `round_robin` rotates one host per booking, `collective` books every host together. `null` means single-host 1-on-1. |
| `service.assign_all_team_members` | `boolean` | Yes | Every current and future team member is auto-assigned as a host. |
| `service.is_rr_weights_enabled` | `boolean` | Yes | Round-robin rotation is biased by each host's `weight`. |
| `service.reschedule_with_same_rr_host` | `boolean` | Yes | Reschedules keep the originally assigned round-robin host. |
| `service.minimum_booking_notice` | `integer` | Yes | Minimum lead time in minutes between booking and slot start. |
| `service.before_event_buffer` | `integer` | Yes | Minutes blocked off before each booking. |
| `service.after_event_buffer` | `integer` | Yes | Minutes blocked off after each booking. |
| `service.booking_limits` | `object` | No | Booking count caps (`BookingLimits` shape); `null` means uncapped. |
| `service.duration_limits` | `object` | No | Booked-minute caps (`DurationLimits` shape); `null` means uncapped. |
| `service.max_active_bookings_per_booker` | `integer \| null` | Yes | Cap on live bookings one booker can hold; `null` means uncapped. |
| `service.period_type` | `"unlimited" \| "rolling" \| "range"` | Yes | Which booking window applies — `unlimited` (any future slot), `rolling` (the next `period_days` days), or `range` (between `period_start_date` and `period_end_date`). |
| `service.period_days` | `integer \| null` | Yes | Size of the rolling window in days, when `period_type` is `rolling`. |
| `service.period_start_date` | `string \| null` | Yes | UTC start of the bookable range, when `period_type` is `range`. |
| `service.period_end_date` | `string \| null` | Yes | UTC end of the bookable range, when `period_type` is `range`. |
| `service.locations` | `object` | No | Locations offered at booking time (array of `ServiceLocation`). |
| `service.recurring_event` | `object` | No | Recurrence configuration (`RecurringEvent` shape); `null` for one-off services. |
| `service.requires_confirmation` | `boolean` | Yes | New bookings start pending until a host confirms them. |
| `service.requires_booker_email_verification` | `boolean` | Yes | Bookers must verify their email before the booking is accepted. |
| `service.disable_guests` | `boolean` | Yes | Bookers cannot add guest attendees. |
| `service.hide_calendar_notes` | `boolean` | Yes | Booking notes are hidden on synced calendar events. |
| `service.hide_calendar_event_details` | `boolean` | Yes | Event details are hidden on synced calendar events. |
| `service.hide_organizer_email` | `boolean` | Yes | The organizer's email is hidden on booking surfaces and notifications. |
| `service.lock_time_zone` | `boolean` | Yes | The booking page is pinned to `time_zone` instead of the visitor's local zone. |
| `service.disable_cancelling` | `boolean` | Yes | Bookers cannot cancel bookings of this service. |
| `service.disable_rescheduling` | `boolean` | Yes | Bookers cannot reschedule bookings of this service. |
| `service.seats_per_time_slot` | `integer \| null` | Yes | Attendee seats a single slot can hold; `null` means seated mode is off. |
| `service.seats_show_attendees` | `boolean` | Yes | Existing attendees are shown to prospective bookers. |
| `service.seats_show_availability_count` | `boolean` | Yes | The remaining-seat count is shown on the booking page. |
| `service.color` | `object` | No | Accent colour (`ServiceColor` shape); `null` uses the workspace default. |
| `service.booking_fields` | `object` | No | Custom booking-form field definitions; `null` when the default form is used. |
| `service.metadata` | `object` | No | Extensible metadata bag; `null` when unset. |
| `service.retired_at` | `string \| null` | Yes | When the service was retired; `null` for live services. |
| `service.created_at` | `string` | Yes | Creation timestamp (RFC 3339). |
| `service.updated_at` | `string` | Yes | Last-update timestamp (RFC 3339). |
| `service.tags` | `array<TagSummary>` | Yes | Workspace tags pinned to this service; `[]` when untagged. |

**`401`** — No valid session cookie or `wbl_*` bearer was present. Returns `ApiErrorEnvelope`.

**`403`** — The actor is a plain member; owner or admin is required (`ADMIN_REQUIRED`). Returns `ApiErrorEnvelope`.

**`404`** — No service visible to the actor with this id in this workspace (`SERVICE_NOT_FOUND`). Returns `ApiErrorEnvelope`.

**`422`** — Path param failed UUID validation (`INVALID_PARAMS`). Returns `ApiErrorEnvelope`.

**`429`** — Rate limit exceeded. The body's `error.code` is `RATE_LIMITED` and `error.details.retry_after` is the same number of seconds as the `Retry-After` header. Returns `ApiErrorEnvelope`.
