Snippets / WordPress

WordPress Custom Post Type — CPT Registration PHP Boilerplate

A clean, well-commented boilerplate for registering custom post types in WordPress — with proper labels, REST API support, archive configuration, and filter hooks for child theme overrides.

PHP utility Production-tested ~5 min setup Theme helper Beginner-friendly WP 6.0+ · PHP 8.0+ Updated May 15, 2026
TL;DR

This WordPress custom post type PHP snippet gives you a clean register_post_type boilerplate with labels, REST API support, archive configuration, and filter hooks for child theme overrides.

Use this when you're building a WordPress theme that needs custom content types — Projects, Testimonials, Team Members, FAQs — and you want a single, reusable function that registers everything with i18n-ready labels, block editor support, archive filtering, and child-theme-safe filter hooks. Copy the final Step 5 file into your theme and you're done.

01 — Building it step by step

WordPress custom post type — The Code

This WordPress custom post type snippet (register_post_type) gives you a complete CPT boilerplate with labels, rewrite settings, and supports.

We’ll build this in 5 progressive steps. Each step is a working checkpoint — you can stop at any point, flush permalinks, and test. We’ll go from the absolute minimum register_post_type() call to a production-grade boilerplate with filter hooks that child themes can extend.

Step 1: The shortest valid registration. WordPress requires three things to register a custom post type: labels (at minimum name and singular_name), public set to true, and has_archive so WordPress generates an archive page at /project/. Without has_archive, the post type exists in the database and admin but has no front-end listing page — visitors get a 404. Paste this in functions.php, visit Settings → Permalinks and click Save (this flushes rewrite rules), then go to /project/ — you’ll see an empty archive page. The “Projects” menu item appears in the WordPress admin sidebar. That’s it — 8 lines of code.

Step 1 — Bare minimum.php
php
// WordPress custom post type — PHP snippet by Mosharaf Hossain
<?php
/**
 * Step 1 — Shortest valid registration.
 * Only the required args: labels (name + singular_name),
 * public=true, and has_archive. No menu icon, no supports.
 */
add_action( 'init', function() {
    register_post_type( 'project', [
        'labels'      => [
            'name'          => 'Projects',
            'singular_name' => 'Project',
        ],
        'public'       => true,
        'has_archive'  => true,
    ] );
} );

Step 2: Translate every label — i18n-ready from day one. The labels array controls every piece of text WordPress shows: the admin menu item (menu_name), the “Add New” button (add_new), the editor heading (edit_item), list-table column text (all_items, search_items), and empty-state messages (not_found, not_found_in_trash). Wrapping each string in __() with a text domain makes this immediately translatable — a German child theme adds .po/.mo files and every label switches to German without touching the registration code. If you skip __() now, you’ll rewrite every string later when internationalization becomes a requirement.

Step 2 — Full i18n labels.php
php
// WordPress custom post type
<?php
/**
 * Step 2 — Complete labels array, every string wrapped
 * in __() for translation readiness. Covers admin menu,
 * editor UI, list table, and screen-reader text.
 */
add_action( 'init', function() {
    register_post_type( 'project', [
        'labels'      => [
            'name'               => __( 'Projects', 'textdomain' ),
            'singular_name'      => __( 'Project', 'textdomain' ),
            'add_new'            => __( 'Add New', 'textdomain' ),
            'add_new_item'       => __( 'Add New Project', 'textdomain' ),
            'edit_item'          => __( 'Edit Project', 'textdomain' ),
            'new_item'           => __( 'New Project', 'textdomain' ),
            'view_item'          => __( 'View Project', 'textdomain' ),
            'search_items'       => __( 'Search Projects', 'textdomain' ),
            'not_found'          => __( 'No projects found.', 'textdomain' ),
            'not_found_in_trash' => __( 'No projects in trash.', 'textdomain' ),
            'all_items'          => __( 'All Projects', 'textdomain' ),
            'archives'           => __( 'Project Archives', 'textdomain' ),
            'menu_name'          => __( 'Projects', 'textdomain' ),
        ],
        'public'       => true,
        'has_archive'  => true,
    ] );
} );

Step 3: Add configuration options — icons, slugs, Gutenberg support. menu_icon sets the Dashicon shown in the admin sidebar — dashicons-portfolio for projects, dashicons-groups for teams, dashicons-format-quote for testimonials. rewrite sets the front-end URL slug — if your post type slug is testimonial but you want URLs to read /reviews/, set rewrite => [ 'slug' => 'reviews' ]. supports controls which editor panels appear: title and editor are essential, thumbnail enables the featured image box, excerpt adds the excerpt textarea, comments allows discussion. Omit any you don’t need. show_in_rest => true enables the Gutenberg block editor — without it you get the classic editor only. template pre-fills new posts with a starter block layout.

Step 3 — Configuration options.php
php
// WordPress custom post type
<?php
/**
 * Step 3 — Production configuration.
 * menu_icon: dashicon for the admin sidebar.
 * rewrite: custom archive slug (yoursite.com/projects).
 * supports: which editor panels appear.
 * show_in_rest: enables Gutenberg block editor.
 * template: default block layout for new posts.
 */
add_action( 'init', function() {
    register_post_type( 'project', [
        'labels'       => [ /* full labels from Step 2 */ ],
        'public'        => true,
        'has_archive'   => true,
        'menu_icon'     => 'dashicons-portfolio',
        'rewrite'       => [ 'slug' => 'projects' ],
        'supports'      => [ 'title', 'editor', 'thumbnail', 'excerpt', 'comments' ],
        'show_in_rest'  => true,
        'template'      => [
            [ 'core/paragraph', [ 'placeholder' => 'Describe this project...' ] ],
            [ 'core/gallery' ],
        ],
    ] );
} );

Step 4: Control the archive query and add a body class. By default, has_archive => true loads 10 posts per page in reverse chronological order — the same as your blog. Use the pre_get_posts action to override this for your CPT archive. The guard clauses — is_admin(), is_main_query(), is_post_type_archive() — prevent the filter from leaking into admin list tables, widget queries, or secondary loops on the same page. Setting posts_per_page to 12 gives you a grid-ready count (2, 3, or 4 columns). The body_class filter adds a CSS hook (.archive-project-page) so you can write archive-specific styles without relying on WordPress’s auto-generated body classes, which change when plugins add post types.

Step 4 — Archive query filters.php
php
// WordPress custom post type
<?php
/**
 * Step 4 — Control the archive page query and add a
 * CSS body class so you can style the archive uniquely.
 *
 * pre_get_posts: fires before WP_Query runs. We check
 * is_main_query() so category widgets don't get modified.
 */
add_action( 'pre_get_posts', function( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }
    if ( ! $query->is_post_type_archive( 'project' ) ) {
        return;
    }
    $query->set( 'posts_per_page', 12 );
    $query->set( 'orderby', 'date' );
    $query->set( 'order', 'DESC' );
} );

add_filter( 'body_class', function( $classes ) {
    if ( is_post_type_archive( 'project' ) ) {
        $classes[] = 'archive-project-page';
    }
    return $classes;
} );

Step 5: Full production boilerplate — filter hooks for everything. The registration moves into a named function hooked to init. Four apply_filters() calls let child themes override labels, the rewrite slug, the menu icon, and the supports array — without copying and modifying the entire function. For example, a child theme that wants a different archive slug adds one line: add_filter( 'theme_project_cpt_slug', fn() => 'portfolio' ). A plugin that adds custom meta boxes appends custom-fields to the supports array via add_filter( 'theme_project_cpt_supports', fn( $s ) => array_merge( $s, [ 'custom-fields' ] ) ). The parent theme’s inc/cpt-project.php file never gets edited — upgrades are safe.

inc/cpt-project.php
php
// WordPress custom post type
<?php
/**
 * CPT Registration Boilerplate — Production Version.
 *
 * Save as inc/cpt-project.php and require from functions.php.
 * Every configurable option is wrapped in apply_filters() so
 * child themes can override labels, slug, icon, and supports
 * without replacing the entire function.
 */
function theme_register_project_cpt() {
    $labels = apply_filters( 'theme_project_cpt_labels', [
        'name'               => __( 'Projects', 'theme' ),
        'singular_name'      => __( 'Project', 'theme' ),
        'add_new_item'       => __( 'Add New Project', 'theme' ),
        'edit_item'          => __( 'Edit Project', 'theme' ),
        'all_items'          => __( 'All Projects', 'theme' ),
        'search_items'       => __( 'Search Projects', 'theme' ),
        'not_found'          => __( 'No projects found.', 'theme' ),
        'not_found_in_trash' => __( 'No projects in trash.', 'theme' ),
        'menu_name'          => __( 'Projects', 'theme' ),
    ] );
    $slug    = apply_filters( 'theme_project_cpt_slug', 'projects' );
    $icon    = apply_filters( 'theme_project_cpt_icon', 'dashicons-portfolio' );
    $support = apply_filters( 'theme_project_cpt_supports', [
        'title', 'editor', 'thumbnail', 'excerpt',
    ] );
    register_post_type( 'project', [
        'labels'       => $labels,
        'public'       => true,
        'has_archive'  => true,
        'menu_icon'    => $icon,
        'rewrite'      => [ 'slug' => $slug ],
        'supports'     => $support,
        'show_in_rest' => true,
    ] );
}
add_action( 'init', 'theme_register_project_cpt' );

Where to put the code

Save Step 5 in inc/cpt-project.php and require it from functions.php:

require_once get_template_directory() . '/inc/cpt-project.php';

After requiring the file, visit Settings → Permalinks and click “Save Changes” — this flushes the rewrite rules so your new slug takes effect. If you skip this step, the archive page will return a 404.

02 — How it works

How WordPress custom post type Works — Why Each Layer Exists

Here is how this WordPress custom post type works step by step: Every argument to register_post_type() solves a real problem. Below we dissect the registration function, i18n labels, rewrite rules and flushing, REST API integration, and filter-based extensibility.

This WordPress custom post type implementation works as follows.

Every argument to register_post_type() solves a real problem. Below we dissect the registration function, i18n labels, rewrite rules and flushing, REST API integration, and filter-based extensibility.

1

register_post_type() — the API, the timing, the minimum args

register_post_type() is WordPress’s API for creating content types beyond posts and pages. It accepts two arguments: the post type slug (a lowercase string, max 20 characters, no spaces — "project") and an associative array of arguments. WordPress stores the configuration in the global $wp_post_types object and generates admin menu items, URL rewrite rules, and database capabilities based on these args. The function must be called on or after the init hook — calling it earlier means WordPress’s internal structures aren’t ready. The minimal viable args are public => true (makes it visible in admin and front-end), has_archive => true (generates an archive page), and labels => [ 'name' => '...', 'singular_name' => '...' ] (provides human-readable text). Everything else falls back to WordPress defaults.

2

Labels for i18n — __(), text domains, and the five label contexts

The labels array uses __() for i18n — each string is a translation key. When WordPress renders the admin interface, it calls get_post_type_labels() which merges your labels with a default set. If you omit add_new_item, WordPress falls back to “Add New Post” — generic and unhelpful. The text domain ('textdomain') should match your theme or plugin slug so translation files are discoverable. A complete labels array covers five contexts: admin menu (name, menu_name), editor screen (add_new_item, edit_item, new_item, view_item), list table (all_items, search_items), empty states (not_found, not_found_in_trash), and front-end (archives). Each context has its own translation string because languages handle these phrases differently — German uses different cases for menu items vs. editor headings.

3

Rewrite rules and flushing — how pretty permalinks actually work

Rewrite rules translate pretty permalinks into query variables. When you set rewrite => [ 'slug' => 'projects' ], WordPress generates internal rules that map /projects/ to index.php?post_type=project and /projects/sample-project/ to the single post query. These rules are stored in the rewrite_rules option in the database — they are not dynamic. Changing a slug requires flushing the rules: go to Settings → Permalinks and click Save (this calls flush_rewrite_rules()). On production, call this once. Never call flush_rewrite_rules() on every page load — it rebuilds the entire rewrite array from scratch, which is an expensive database operation (reads all registered post types, all taxonomies, and all custom rewrite structures, then writes a large serialized array to the options table).

4

REST API integration — show_in_rest, block editor, and template defaults

show_in_rest => true enables the WordPress REST API for this post type, which is required for the Gutenberg block editor. Behind the scenes, WordPress registers REST routes at /wp-json/wp/v2/project and /wp-json/wp/v2/project/{id} with standard CRUD endpoints. The block editor uses these routes to load and save post data. If you set show_in_rest => false, the classic editor loads instead. The template argument pre-fills the block editor with a default layout — pass an array of block definitions (each is [ 'namespace/block-name', [ 'attribute' => 'value' ] ]) and new posts start with that structure. Users can modify or delete the template blocks; this is a starting point, not a locked layout (for locked layouts, use template_lock).

5

Filter hooks — child theme extensibility without editing parent files

Filter hooks are the child-theme safety net. Every apply_filters() call in the production boilerplate creates an extension point. A child theme’s functions.php can modify any of the four configurable arrays with a single add_filter() line. This matters for theme shops and agencies: the parent theme ships with sensible defaults, and each client’s child theme overrides only what’s different — the slug for one client, the icon for another. The parent theme can be updated (bug fixes, new features) without overwriting client customizations because the customizations live in filters, not in edited copies of the parent theme’s files. This is the same pattern WordPress core uses: register defaults, expose filters, let downstream code modify.

03 — Setup & integration

WordPress custom post type — Setup & Integration

To integrate this WordPress custom post type into your project: One PHP file, one require line, and your custom post type is live. Here's the folder structure, integration steps, and testing checklist.

This WordPress custom post type implementation works as follows.

One PHP file, one require line, and your custom post type is live. Here’s the folder structure, integration steps, and testing checklist.

wp-content/themes/your-theme/
│   ├── inc/
│   │   └── cpt-project.php
    └── functions.php
1

Place the file in inc/ and require from functions.php

Place the file and require it. Copy the Step 5 code into inc/cpt-project.php. In your theme’s functions.php, add this line near the top — above any template-specific includes but after the theme setup function:

require_once get_template_directory() . '/inc/cpt-project.php';

If you’re using a child theme, require from the child theme’s functions.php — the filter hooks in the boilerplate let you customize without touching the parent file. For multiple post types, create one file per post type (cpt-testimonial.php, cpt-faq.php) and require each. This keeps the registration for each post type isolated and easier to debug.

2

Flush permalinks — the one step everyone forgets

Flush permalinks. After adding the require line, visit Settings → Permalinks in the WordPress admin and click “Save Changes”. You don’t need to change any settings — the save action triggers flush_rewrite_rules() internally, which rebuilds the URL routing table. Without this step, /projects/ returns a 404 because WordPress doesn’t know the rewrite rule for your new post type yet. If you deploy via version control, add a one-time flush to your deploy script — or use a plugin activation hook if you’re shipping this as part of a plugin.

3

Test the archive page — template hierarchy and fallback templates

Test the archive page. Navigate to yoursite.com/projects/ (or whatever slug you set). You should see an archive page — likely empty if you haven’t created any projects yet. WordPress uses archive.php or index.php as the fallback template. For a custom archive design, create archive-project.php in your theme root — WordPress’s template hierarchy will automatically load it for this post type. Test with at least one published project post to verify the loop displays content correctly.

4

Test the single page and admin list table

Test the single page. Create a new project (Projects → Add New), publish it, and view it on the front-end at /projects/your-project-slug/. WordPress uses single.php or singular.php as the fallback. For a custom single template, create single-project.php. Verify that the title, content, featured image, and any custom fields render correctly. Check the admin list table (Projects → All Projects) — the columns should show the title and date, and the “Add New” button at the top should read “Add New Project” (the label from Step 2).

04 — Making it your own

Making This WordPress custom post type Your Own

Customise this WordPress custom post type to match your specific needs: The boilerplate is a starting point. Extend it with custom admin columns, taxonomy support, multi-CPT factory loops, and static-page-to-CPT conversions.

1

Add custom admin columns — client name, status, date range

Add custom admin columns. The default list table shows title and date — not useful for a Project post type that has a client name or status field. Use the manage_{$post_type}_posts_columns filter to add columns and manage_{$post_type}_posts_custom_column to populate them:

add_filter( 'manage_project_posts_columns', function( $cols ) {n    $cols['client']  = __( 'Client', 'theme' );n    $cols['status']  = __( 'Status', 'theme' );n    return $cols;n} );nnadd_action( 'manage_project_posts_custom_column', function( $col, $id ) {n    if ( 'client' === $col ) echo esc_html( get_field( 'client_name', $id ) );n    if ( 'status' === $col ) echo esc_html( get_field( 'project_status', $id ) );n}, 10, 2 );

These hooks fire only for the project list table — you can add different columns for each post type. The $id parameter is the post ID, so you can pull any post meta, taxonomy terms, or computed values. For sortable columns, add a third hook (manage_edit-project_sortable_columns) that maps your column to an orderby value.

2

Add taxonomy support — categories and tags for your post type

Add taxonomy support. A Project post type often needs categories (project type, industry) and tags (technology, client). Register a custom taxonomy and attach it to your post type:

function theme_register_project_taxonomies() {n    register_taxonomy( 'project_type', 'project', [n        'labels'            => [n            'name'          => __( 'Project Types', 'theme' ),n            'singular_name' => __( 'Project Type', 'theme' ),n        ],n        'hierarchical'      => true,  // Category-like (checkboxes).n        'show_in_rest'      => true,  // Enable in block editor sidebar.n        'rewrite'           => [ 'slug' => 'project-type' ],n    ] );n}nadd_action( 'init', 'theme_register_project_taxonomies' );

hierarchical => true makes it category-like with parent/child relationships. Set to false for flat, tag-like taxonomies. show_in_rest => true puts the taxonomy panel in the block editor sidebar under the “Post” tab. Register the taxonomy on init (same hook as the post type) — order doesn’t matter because both are registered before WordPress processes any request. For a post type that needs both categories and tags, you can also pass 'taxonomies' => [ 'category', 'post_tag' ] inside the register_post_type() args to reuse built-in taxonomies.

3

Register multiple post types from a single config array

Register multiple post types from a single config array. If your theme registers Projects, Testimonials, and FAQs, convert the boilerplate into a loop-driven factory that reads from a configuration array:

function theme_register_all_cpts() {n    $types = apply_filters( 'theme_cpt_config', [n        [n            'slug'    => 'project',n            'rewrite' => 'projects',n            'icon'    => 'dashicons-portfolio',n            'singular' => 'Project', 'plural' => 'Projects',n        ],n        [n            'slug'    => 'testimonial',n            'rewrite' => 'testimonials',n            'icon'    => 'dashicons-format-quote',n            'singular' => 'Testimonial', 'plural' => 'Testimonials',n        ],n    ] );n    foreach ( $types as $cpt ) {n        $labels = [n            'name'          => $cpt['plural'],n            'singular_name' => $cpt['singular'],n            'add_new_item'  => sprintf( __( 'Add New %s', 'theme' ), $cpt['singular'] ),n            'edit_item'     => sprintf( __( 'Edit %s', 'theme' ), $cpt['singular'] ),n            'all_items'     => sprintf( __( 'All %s', 'theme' ), $cpt['plural'] ),n        ];n        register_post_type( $cpt['slug'], [n            'labels'      => $labels,n            'public'      => true,n            'has_archive' => true,n            'menu_icon'   => $cpt['icon'],n            'rewrite'     => [ 'slug' => $cpt['rewrite'] ],n            'supports'    => [ 'title', 'editor', 'thumbnail' ],n            'show_in_rest' => true,n        ] );n    }n}nadd_action( 'init', 'theme_register_all_cpts' );

The child theme adds or removes post types by filtering theme_cpt_config. A client that doesn’t need Testimonials removes that entry from the array. A client that adds a Portfolio post type appends a new entry. The registration loop copies the same structure from each entry — supports, rewrite slug, and icon — so all post types share consistent behavior.

4

Convert a static page section to a CPT — Team, FAQ, Services

Converting a static page section to a CPT. Many sites have a “Team” section built as a static page with hardcoded HTML. Converting it to a Team Members CPT makes it editable through the WordPress admin. The process: (1) Register the CPT using this boilerplate. (2) Build archive-team.php and single-team.php templates that replicate the static page’s HTML structure — replace hardcoded member data with the WordPress loop. (3) Copy each team member’s content into individual CPT posts. (4) Replace the static page’s content with a redirect or a custom page template that queries the CPT. (5) Delete or unpublish the static page once the CPT archive matches it. The pre_get_posts hook from Step 4 controls the display order (alphabetical by last name, by hire date, by custom sort order field).

3 pitfalls that will silently break your CPT:

1. Forgetting to flush permalinks after registration. Rewrite rules are stored in the database and only rebuild when flush_rewrite_rules() runs. Adding a new post type but skipping the Permalinks → Save step means every URL for that post type returns 404. This is the most common CPT deployment bug. On production, flush once via Settings → Permalinks. In local development, the rules auto-flush when you visit the Permalinks page.

2. Slug collision with an existing page. If you have a WordPress page at /projects/ and register a CPT with rewrite => [ 'slug' => 'projects' ], the page takes priority and your CPT archive becomes inaccessible. WordPress resolves URL conflicts by checking pages first, then post type archives. Fix: rename the page slug or set has_archive => 'our-projects' (a custom archive slug that doesn’t conflict).

3. Calling register_post_type() before init or wrapping it in a conditional. CPT registration must happen on every request, unconditionally, on the init hook. If you wrap it in if ( is_admin() ), the post type won’t exist on front-end requests — archive pages, REST API calls, and search queries all fail. If you call it directly in functions.php without the init hook, WordPress internals aren’t loaded yet. Always use add_action( 'init', 'function_name' ).

Rewrite rules and performance — what actually happens:

WordPress stores all rewrite rules in a single rewrite_rules option in the wp_options table. Every page request loads this option (it’s autoloaded). The array contains hundreds of regex patterns — one entry per post type archive, per taxonomy, per page slug, per attachment, per feed endpoint, per pagination rule. A site with 5 custom post types and 3 taxonomies has roughly 400–600 rewrite rules. This is a serialized PHP array — on a typical site, it’s 40–80 KB. WordPress iterates through these rules on every request to match the current URL. The cost is negligible (sub-millisecond for 500 regex checks on PHP 8.x), but it scales linearly with the number of registered rewrite structures.

What NOT to do: Never call flush_rewrite_rules() on init or on every page load. This function deletes the option, rebuilds it from scratch by calling WP_Rewrite::rewrite_rules() (which iterates every registered post type, taxonomy, and custom rewrite structure), and writes a new serialized array to the database. On a site with 5 post types, this is 5–10 database queries per page load. Multiply by concurrent visitors and you’ve created a self-inflicted performance problem. The correct pattern: flush once after registration changes, then let the rules persist.

0 comments

No comments yet. Be the first.

Leave a Reply

Table of Contents

WordPress custom post type — Code Snippet

WordPress custom post type — code snippet by Mosharaf Hossain

This WordPress custom post type is a production-tested PHP snippet for WordPress developers. See the WordPress register_post_type documentation for related official documentation.

The WordPress custom post type function is the foundation of every theme that manages more than posts and pages. Projects, Testimonials, Team Members, FAQs — each is a custom post type. Most examples online show the minimum three arguments and stop there. This production boilerplate adds everything a real project needs: full i18n labels, REST API support, archive configuration, pre_get_posts control, and apply_filters hooks on every configurable option so child themes can override labels, slugs, icons, and supports without editing the parent theme file.

The snippet builds in five steps that each run as a working checkpoint. Step 1 is eight lines — the shortest valid registration that creates an admin menu item and an archive page. Step 2 completes the labels array with __() wrappers for every string WordPress uses: admin menu, editor headings, list table columns, and empty-state messages. Step 3 adds a custom menu icon, a rewrite slug, show_in_rest for the block editor, and a default block template. Step 4 hooks pre_get_posts to control how many posts appear on the archive and in what order.

Step 5 wraps all configurable values in apply_filters() so any child theme can change the slug, icon, or supports with a single add_filter() line.

The file belongs in inc/cpt-project.php, required from functions.php with require_once get_template_directory() . "/inc/cpt-project.php". After adding the file, visit Settings then Permalinks and click Save to flush rewrite rules. Without this step, every URL for the new post type returns 404 because WordPress has not added the routing rules yet.

The three most common mistakes: forgetting to flush permalinks, registering a slug that matches an existing page slug (the page takes priority and the archive becomes unreachable), and calling register_post_type before the init hook (WordPress internals are not yet ready).

Tested on WordPress 6.0+ with PHP 8.0+. Compatible with the block editor and the classic editor. Works in parent and child themes. To control posts per archive page after registration, pair this with the WordPress Archive Query Posts Per Page snippet. Browse all WordPress code snippets at Code Snippets. This boilerplate is the starting point for every custom post type in the Vitamines project and across other client builds.

Related: Archive Query snippet — and browse the full code snippet library.

Chat on WhatsApp