diff --git a/CRM/Core/BAO/EntityTag.php b/CRM/Core/BAO/EntityTag.php
index daa8568faae9658277f4b511e68fd54b23ace127..65d5587b7dbc3f28433c56f16f96f2733c3ae939 100644
--- a/CRM/Core/BAO/EntityTag.php
+++ b/CRM/Core/BAO/EntityTag.php
@@ -96,7 +96,7 @@ class CRM_Core_BAO_EntityTag extends CRM_Core_DAO_EntityTag {
    * Delete the tag for a contact.
    *
    * @param array $params
-   *
+   * @deprecated
    * WARNING: Nonstandard params searches by tag_id rather than id!
    */
   public static function del(&$params) {
diff --git a/Civi/Api4/Action/Contact/Delete.php b/Civi/Api4/Action/Contact/Delete.php
new file mode 100644
index 0000000000000000000000000000000000000000..5d2267441ac8d23bd68c563748e49879cfdda2e9
--- /dev/null
+++ b/Civi/Api4/Action/Contact/Delete.php
@@ -0,0 +1,37 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Action\Contact;
+
+/**
+ * Deletes a contact, by default moving to trash. Set `useTrash = FALSE` for permanent deletion.
+ * @inheritDoc
+ */
+class Delete extends \Civi\Api4\Generic\DAODeleteAction {
+  use \Civi\Api4\Generic\Traits\SoftDelete;
+
+  /**
+   * @param $items
+   * @return array
+   * @throws \API_Exception
+   */
+  protected function deleteObjects($items) {
+    foreach ($items as $item) {
+      if (!\CRM_Contact_BAO_Contact::deleteContact($item['id'], FALSE, !$this->useTrash, $this->checkPermissions)) {
+        throw new \API_Exception("Could not delete {$this->getEntityName()} id {$item['id']}");
+      }
+      $ids[] = ['id' => $item['id']];
+    }
+    return $ids;
+  }
+
+}
diff --git a/Civi/Api4/Contact.php b/Civi/Api4/Contact.php
index 1fe634a0859cf541ca8a028707f07f57956c0b95..34f6463f5530f8c5f28962f67c3330352255d88f 100644
--- a/Civi/Api4/Contact.php
+++ b/Civi/Api4/Contact.php
@@ -26,6 +26,15 @@ namespace Civi\Api4;
  */
 class Contact extends Generic\DAOEntity {
 
+  /**
+   * @param bool $checkPermissions
+   * @return Action\Contact\Delete
+   */
+  public static function delete($checkPermissions = TRUE) {
+    return (new Action\Contact\Delete(__CLASS__, __FUNCTION__))
+      ->setCheckPermissions($checkPermissions);
+  }
+
   /**
    * @param bool $checkPermissions
    * @return Action\Contact\GetChecksum
diff --git a/Civi/Api4/Generic/DAODeleteAction.php b/Civi/Api4/Generic/DAODeleteAction.php
index b2b9584e921db92494b24ae08eba4e2340de2c38..84d818de9b1c51d34b46a547a2eb583a9e5aa0c5 100644
--- a/Civi/Api4/Generic/DAODeleteAction.php
+++ b/Civi/Api4/Generic/DAODeleteAction.php
@@ -14,6 +14,7 @@ namespace Civi\Api4\Generic;
 
 use Civi\API\Exception\UnauthorizedException;
 use Civi\Api4\Utils\CoreUtil;
+use Civi\Api4\Utils\ReflectionUtils;
 
 /**
  * Delete one or more $ENTITIES.
@@ -56,7 +57,8 @@ class DAODeleteAction extends AbstractBatchAction {
     $ids = [];
     $baoName = $this->getBaoName();
 
-    if ($this->getEntityName() !== 'EntityTag' && method_exists($baoName, 'del')) {
+    // Use BAO::del() method if it is not deprecated
+    if (method_exists($baoName, 'del') && !ReflectionUtils::isMethodDeprecated($baoName, 'del')) {
       foreach ($items as $item) {
         $args = [$item['id']];
         $bao = call_user_func_array([$baoName, 'del'], $args);
diff --git a/Civi/Api4/Generic/Traits/SoftDelete.php b/Civi/Api4/Generic/Traits/SoftDelete.php
new file mode 100644
index 0000000000000000000000000000000000000000..108495d7fc182db925c92a9292d6b6b07fbf2c45
--- /dev/null
+++ b/Civi/Api4/Generic/Traits/SoftDelete.php
@@ -0,0 +1,27 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Generic\Traits;
+
+/**
+ * This trait is used by entities with a "move to trash" option.
+ * @method $this setUseTrash(bool $useTrash) Pass FALSE to force delete and bypass trash
+ * @method bool getUseTrash()
+ */
+trait SoftDelete {
+
+  /**
+   * Should $ENTITY be moved to the trash instead of permanently deleted?
+   * @var bool
+   */
+  protected $useTrash = TRUE;
+
+}
diff --git a/Civi/Api4/Utils/ReflectionUtils.php b/Civi/Api4/Utils/ReflectionUtils.php
index a5d59803f13a46770cd8ee83b4a3e7beb6abf293..0263718e3aa44b82a5bd78cbcc3e373536910bfb 100644
--- a/Civi/Api4/Utils/ReflectionUtils.php
+++ b/Civi/Api4/Utils/ReflectionUtils.php
@@ -179,6 +179,20 @@ class ReflectionUtils {
     }
   }
 
+  /**
+   * Check if a class method is deprecated
+   *
+   * @param string $className
+   * @param string $methodName
+   * @return bool
+   * @throws \ReflectionException
+   */
+  public static function isMethodDeprecated(string $className, string $methodName): bool {
+    $reflection = new \ReflectionClass($className);
+    $docBlock = $reflection->getMethod($methodName)->getDocComment();
+    return strpos($docBlock, "@deprecated") !== FALSE;
+  }
+
   /**
    * Find any methods in this class which match the given prefix.
    *
diff --git a/Civi/Test/Api3TestTrait.php b/Civi/Test/Api3TestTrait.php
index 65bd2ee2162e5c167d6bc1a56ef70513a13d937c..dafe7e70a63ad275eb59c093ab2f181335f5d8dd 100644
--- a/Civi/Test/Api3TestTrait.php
+++ b/Civi/Test/Api3TestTrait.php
@@ -510,6 +510,10 @@ trait Api3TestTrait {
       }
     }
 
+    if (isset($actionInfo[0]['params']['useTrash'])) {
+      $v4Params['useTrash'] = empty($v3Params['skip_undelete']);
+    }
+
     // Build where clause for 'getcount', 'getsingle', 'getvalue', 'get' & 'replace'
     if ($v4Action == 'get' || $v3Action == 'replace') {
       foreach ($v3Params as $key => $val) {
diff --git a/tests/phpunit/api/v3/ContactTest.php b/tests/phpunit/api/v3/ContactTest.php
index b161260412fe4871080c11ba26a4258cbecdc412..af6e0df138917e8d9b46ffe5601b846e396a5e0c 100644
--- a/tests/phpunit/api/v3/ContactTest.php
+++ b/tests/phpunit/api/v3/ContactTest.php
@@ -3154,14 +3154,17 @@ class api_v3_ContactTest extends CiviUnitTestCase {
 
   /**
    * Test that delete with skip undelete respects permissions.
-   * TODO: Api4
+   *
+   * @param int $version
    *
    * @throws \CRM_Core_Exception
-   * @throws \CiviCRM_API3_Exception
+   * @dataProvider versionThreeAndFour
    */
-  public function testContactDeletePermissions(): void {
+  public function testContactDeletePermissions(int $version): void {
+    $this->_apiversion = $version;
     $contactID = $this->individualCreate();
-    $tag = $this->callAPISuccess('Tag', 'create', ['name' => 'to be deleted']);
+    $this->quickCleanup(['civicrm_entity_tag', 'civicrm_tag']);
+    $tag = $this->callAPISuccess('Tag', 'create', ['name' => uniqid('to be deleted')]);
     $this->callAPISuccess('EntityTag', 'create', ['entity_id' => $contactID, 'tag_id' => $tag['id']]);
     CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM'];
     $this->callAPIFailure('Contact', 'delete', [
@@ -3169,12 +3172,14 @@ class api_v3_ContactTest extends CiviUnitTestCase {
       'check_permissions' => 1,
       'skip_undelete' => 1,
     ]);
+    $this->callAPISuccessGetCount('EntityTag', ['entity_id' => $contactID], 1);
     $this->callAPISuccess('Contact', 'delete', [
       'id' => $contactID,
       'check_permissions' => 0,
       'skip_undelete' => 1,
     ]);
     $this->callAPISuccessGetCount('EntityTag', ['entity_id' => $contactID], 0);
+    $this->quickCleanup(['civicrm_entity_tag', 'civicrm_tag']);
   }
 
   /**
diff --git a/tests/phpunit/api/v4/Entity/ConformanceTest.php b/tests/phpunit/api/v4/Entity/ConformanceTest.php
index 85a069a180cf20e2106d9f4dfd47f05c03df2b89..0b22857262ed3124d131171f4ab0edfa2da0c7ab 100644
--- a/tests/phpunit/api/v4/Entity/ConformanceTest.php
+++ b/tests/phpunit/api/v4/Entity/ConformanceTest.php
@@ -424,10 +424,15 @@ class ConformanceTest extends UnitTestCase implements HookInterface {
     $this->assertEquals(0, $this->checkAccessCounts["{$entity}::delete"]);
     $isReadOnly = $this->isReadOnly($entityClass);
 
-    $deleteResult = $entityClass::delete()
+    $deleteAction = $entityClass::delete()
       ->setCheckPermissions(!$isReadOnly)
-      ->addWhere('id', '=', $id)
-      ->execute();
+      ->addWhere('id', '=', $id);
+
+    if (property_exists($deleteAction, 'useTrash')) {
+      $deleteAction->setUseTrash(FALSE);
+    }
+
+    $deleteResult = $deleteAction->execute();
 
     // should get back an array of deleted id
     $this->assertEquals([['id' => $id]], (array) $deleteResult);
diff --git a/tests/phpunit/api/v4/Mock/MockV4ReflectionGrandchild.php b/tests/phpunit/api/v4/Mock/MockV4ReflectionGrandchild.php
index 6c0700f2953131fd204b69a4fb297a243f555c0a..5b2ebeeba6b6186a5dbf0b41dd1484be733b0afe 100644
--- a/tests/phpunit/api/v4/Mock/MockV4ReflectionGrandchild.php
+++ b/tests/phpunit/api/v4/Mock/MockV4ReflectionGrandchild.php
@@ -31,4 +31,21 @@ namespace api\v4\Mock;
  */
 class MockV4ReflectionGrandchild extends MockV4ReflectionChild {
 
+  /**
+   * Function marked deprecated
+   * @see \api\v4\Utils\ReflectionUtilsTest::testIsMethodDeprecated
+   * @deprecated
+   */
+  public static function deprecatedFn() {
+
+  }
+
+  /**
+   * Function not marked deprecated
+   * @see \api\v4\Utils\ReflectionUtilsTest::testIsMethodDeprecated
+   */
+  public static function nonDeprecatedFn() {
+
+  }
+
 }
diff --git a/tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php b/tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php
index 868ae79d640c5d15d5cfa9453c8bce18c6982fa4..bd6d218a6d784c074ee52b24649b2a4d4e796961 100644
--- a/tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php
+++ b/tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php
@@ -109,4 +109,10 @@ This is the base class.';
     $this->assertEquals($expected, ReflectionUtils::parseDocBlock($input));
   }
 
+  public function testIsMethodDeprecated() {
+    $mockClass = 'api\v4\Mock\MockV4ReflectionGrandchild';
+    $this->assertTrue(ReflectionUtils::isMethodDeprecated($mockClass, 'deprecatedFn'));
+    $this->assertFalse(ReflectionUtils::isMethodDeprecated($mockClass, 'nonDeprecatedFn'));
+  }
+
 }