> For the complete documentation index, see [llms.txt](https://docs.acpt.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.acpt.io/integrations/abilities-api/custom-post-types.md).

# Custom post types

acpt/list-post-types, acpt/get-post-type, acpt/save-post-type, acpt/delete-post-type

Category: `acpt-content-model` · Default capability: `manage_options`

| Ability                 | What it does                                                  |
| ----------------------- | ------------------------------------------------------------- |
| `acpt/list-post-types`  | Returns every custom post type registered via ACPT            |
| `acpt/get-post-type`    | Returns a single custom post type by slug                     |
| `acpt/save-post-type`   | Creates a post type, or updates it if the slug already exists |
| `acpt/delete-post-type` | Deletes a post type definition                                |

## acpt/list-post-types

No input required.

```php
$result = wp_get_ability('acpt/list-post-types')->execute();

// $result['data'] is an array of post type objects, shaped like the
// acpt/get-post-type example below.
```

## acpt/get-post-type

**Input**

| Property    | Type   | Required | Description                                |
| ----------- | ------ | -------- | ------------------------------------------ |
| `post_type` | string | Yes      | The post type technical name (`post_name`) |

```php
$result = wp_get_ability('acpt/get-post-type')->execute([
    'post_type' => 'movie',
]);
```

```json
{
  "success": true,
  "message": "Operation completed successfully.",
  "data": {
    "id": "0a3154e2-0bc6-49a4-b91d-bc8fec485e57",
    "name": "movie",
    "singular": "Movie",
    "plural": "Movies",
    "icon": "dashicons-admin-post",
    "isNative": false,
    "postCount": 12,
    "supports": ["title", "editor", "thumbnail"],
    "labels": { "menu_name": "Movies", "add_new": "Add Movie" },
    "settings": { "public": true, "show_in_rest": true },
    "taxonomies": ["genre"],
    "permissions": [],
    "isWooCommerce": false,
    "woocommerceProductData": []
  }
}
```

{% hint style="warning" %}
**Response field names differ from input field names.** The response uses `name`/`singular`/`plural` (matching ACPT's stored post type model), while `acpt/save-post-type` expects `post_name`/`singular_label`/`plural_label` (matching the underlying `register_acpt_post_type()` function). Copy-pasting a `get`/`list` response straight into a `save` call won't work for these fields — same naming split as [taxonomies](/integrations/abilities-api/taxonomies.md). `icon` also round-trips differently: you send a bare Dashicon slug (`tickets`), the response returns it prefixed (`dashicons-tickets`), matching what WordPress actually registers.
{% endhint %}

If `post_type` doesn't match an existing ACPT post type, the ability returns a `WP_Error` with code `acpt_ability_failed`.

## acpt/save-post-type

**Input**

| Property         | Type           | Required | Notes                                                                                                                                                                                                                                                      |
| ---------------- | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `post_name`      | string         | Yes      | Max 20 characters. If a post type with this slug already exists, it's **updated** instead of duplicated                                                                                                                                                    |
| `singular_label` | string         | Yes      |                                                                                                                                                                                                                                                            |
| `plural_label`   | string         | Yes      |                                                                                                                                                                                                                                                            |
| `icon`           | string         | No       | Any [Dashicon](https://developer.wordpress.org/resource/dashicons/) slug, e.g. `admin-post`. Defaults to `admin-post` if omitted                                                                                                                           |
| `supports`       | array\<string> | No       | `title`, `editor`, `thumbnail`, `excerpt`, `author`, `trackbacks`, `custom-fields`, `comments`, `revisions`, `page-attributes`, `post-formats`. Defaults to `["title", "editor"]` if omitted                                                               |
| `labels`         | object         | No       | Any subset of WordPress's [post type labels](https://developer.wordpress.org/reference/functions/register_post_type/) (`menu_name`, `add_new`, `edit_item`, …). Defaults to a full set derived from `singular_label`/`plural_label` if omitted — see below |
| `settings`       | object         | No       | Any subset of `register_post_type()`'s arguments — `public`, `show_in_menu`, `show_in_rest`, `has_archive`, `rewrite`, `menu_position`, `capability_type`, etc. Defaults to a full, working set if omitted — see below                                     |

```php
$result = wp_get_ability('acpt/save-post-type')->execute([
    'post_name'      => 'movie',
    'singular_label' => 'Movie',
    'plural_label'   => 'Movies',
    'icon'           => 'admin-post',
    'supports'       => ['title', 'editor', 'thumbnail'],
    'labels'         => ['menu_name' => 'Movies'],
    'settings'       => ['public' => true, 'show_in_rest' => true, 'has_archive' => true],
]);
```

The response `data` is the same shape as `acpt/get-post-type`'s.

### Defaults when fields are omitted

`icon`, `supports`, `labels`, and `settings` are all optional. Omitting one of them entirely fills it in with the same values the admin UI's creation wizard pre-selects for a brand-new post type — the result behaves identically to one created through the UI without touching any toggle. This is different from sending an **explicit empty value**: `"supports": []`/`"labels": {}`/`"settings": {}` is respected as-is (a deliberately bare post type), while leaving the key out entirely gets the defaults below.

| Field      | Default when omitted                                                                                                                                                                                                                                                                  |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `icon`     | `admin-post`                                                                                                                                                                                                                                                                          |
| `supports` | `["title", "editor"]`                                                                                                                                                                                                                                                                 |
| `labels`   | Derived from `singular_label`/`plural_label` — `add_new_item: "Add new {singular}"`, `edit_item: "Edit {singular}"`, `not_found: "No {singular} found"`, etc. (every label WordPress's `register_post_type()` accepts)                                                                |
| `settings` | `public: true`, `publicly_queryable: true`, `show_ui: true`, `show_in_menu: true`, `show_in_nav_menus: true`, `show_in_admin_bar: true`, `show_in_rest: true`, `capability_type: "post"`, `has_archive: "<post_name>"` (the post type's own slug), `rewrite: true`, `query_var: true` |

Sending a *partial* `labels`/`settings` object (some keys, not all) does **not** trigger these defaults for the missing keys — only a fully omitted field does. Whatever you send for `labels`/`settings` is used as-is.

## acpt/delete-post-type

**Input**

| Property       | Type    | Required | Description                                                      |
| -------------- | ------- | -------- | ---------------------------------------------------------------- |
| `post_type`    | string  | Yes      | The post type slug to delete                                     |
| `delete_posts` | boolean | No       | `true` also deletes every post of that type. Defaults to `false` |

```php
$result = wp_get_ability('acpt/delete-post-type')->execute([
    'post_type'     => 'movie',
    'delete_posts'  => false,
]);
```

```json
{
  "success": true,
  "message": "Operation completed successfully."
}
```
