diff --git a/CRM/Stripe/BAO/StripeCustomer.php b/CRM/Stripe/BAO/StripeCustomer.php
new file mode 100644
index 0000000000000000000000000000000000000000..cdf41ccd3365733fdfb684e251e89c68a92c64ec
--- /dev/null
+++ b/CRM/Stripe/BAO/StripeCustomer.php
@@ -0,0 +1,26 @@
+<?php
+use CRM_Stripe_ExtensionUtil as E;
+
+class CRM_Stripe_BAO_StripeCustomer extends CRM_Stripe_DAO_StripeCustomer {
+
+  /**
+   * Create a new StripeCustomer based on array-data
+   *
+   * @param array $params key-value pairs
+   * @return CRM_Stripe_DAO_StripeCustomer|NULL
+   *
+  public static function create($params) {
+    $className = 'CRM_Stripe_DAO_StripeCustomer';
+    $entityName = 'StripeCustomer';
+    $hook = empty($params['id']) ? 'create' : 'edit';
+
+    CRM_Utils_Hook::pre($hook, $entityName, CRM_Utils_Array::value('id', $params), $params);
+    $instance = new $className();
+    $instance->copyValues($params);
+    $instance->save();
+    CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance);
+
+    return $instance;
+  } */
+
+}
diff --git a/CRM/Stripe/DAO/StripeCustomer.php b/CRM/Stripe/DAO/StripeCustomer.php
new file mode 100644
index 0000000000000000000000000000000000000000..afb8ccc8d5b60f73879e77aa8da94191225a110d
--- /dev/null
+++ b/CRM/Stripe/DAO/StripeCustomer.php
@@ -0,0 +1,235 @@
+<?php
+
+/**
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ *
+ * Generated from com.drastikbydesign.stripe/xml/schema/CRM/Stripe/StripeCustomer.xml
+ * DO NOT EDIT.  Generated by CRM_Core_CodeGen
+ * (GenCodeChecksum:2ce3243ca4d4f01342a48d0ef1ac2d99)
+ */
+use CRM_Stripe_ExtensionUtil as E;
+
+/**
+ * Database access object for the StripeCustomer entity.
+ */
+class CRM_Stripe_DAO_StripeCustomer extends CRM_Core_DAO {
+  const EXT = E::LONG_NAME;
+  const TABLE_ADDED = '';
+
+  /**
+   * Static instance to hold the table name.
+   *
+   * @var string
+   */
+  public static $_tableName = 'civicrm_stripe_customers';
+
+  /**
+   * Primary key field(s).
+   *
+   * @var string[]
+   */
+  public static $_primaryKey = [];
+
+  /**
+   * Should CiviCRM log any modifications to this table in the civicrm_log table.
+   *
+   * @var bool
+   */
+  public static $_log = TRUE;
+
+  /**
+   * The Stripe Customer ID
+   *
+   * @var string|null
+   *   (SQL type: varchar(255))
+   *   Note that values will be retrieved from the database as a string.
+   */
+  public $id;
+
+  /**
+   * FK to Contact
+   *
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
+   */
+  public $contact_id;
+
+  /**
+   * Foreign key to civicrm_payment_processor.id
+   *
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
+   */
+  public $processor_id;
+
+  /**
+   * Class constructor.
+   */
+  public function __construct() {
+    $this->__table = 'civicrm_stripe_customers';
+    parent::__construct();
+  }
+
+  /**
+   * Returns localized title of this entity.
+   *
+   * @param bool $plural
+   *   Whether to return the plural version of the title.
+   */
+  public static function getEntityTitle($plural = FALSE) {
+    return $plural ? E::ts('Stripe Customers') : E::ts('Stripe Customer');
+  }
+
+  /**
+   * Returns foreign keys and entity references.
+   *
+   * @return array
+   *   [CRM_Core_Reference_Interface]
+   */
+  public static function getReferenceColumns() {
+    if (!isset(Civi::$statics[__CLASS__]['links'])) {
+      Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__);
+      Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'contact_id', 'civicrm_contact', 'id');
+      CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']);
+    }
+    return Civi::$statics[__CLASS__]['links'];
+  }
+
+  /**
+   * Returns all the column names of this table
+   *
+   * @return array
+   */
+  public static function &fields() {
+    if (!isset(Civi::$statics[__CLASS__]['fields'])) {
+      Civi::$statics[__CLASS__]['fields'] = [
+        'id' => [
+          'name' => 'id',
+          'type' => CRM_Utils_Type::T_STRING,
+          'title' => E::ts('Stripe Customer ID'),
+          'description' => E::ts('The Stripe Customer ID'),
+          'maxlength' => 255,
+          'size' => CRM_Utils_Type::HUGE,
+          'where' => 'civicrm_stripe_customers.id',
+          'table_name' => 'civicrm_stripe_customers',
+          'entity' => 'StripeCustomer',
+          'bao' => 'CRM_Stripe_DAO_StripeCustomer',
+          'localizable' => 0,
+          'add' => NULL,
+        ],
+        'contact_id' => [
+          'name' => 'contact_id',
+          'type' => CRM_Utils_Type::T_INT,
+          'description' => E::ts('FK to Contact'),
+          'where' => 'civicrm_stripe_customers.contact_id',
+          'table_name' => 'civicrm_stripe_customers',
+          'entity' => 'StripeCustomer',
+          'bao' => 'CRM_Stripe_DAO_StripeCustomer',
+          'localizable' => 0,
+          'FKClassName' => 'CRM_Contact_DAO_Contact',
+          'add' => NULL,
+        ],
+        'processor_id' => [
+          'name' => 'processor_id',
+          'type' => CRM_Utils_Type::T_INT,
+          'title' => E::ts('Payment Processor ID'),
+          'description' => E::ts('Foreign key to civicrm_payment_processor.id'),
+          'where' => 'civicrm_stripe_customers.processor_id',
+          'table_name' => 'civicrm_stripe_customers',
+          'entity' => 'StripeCustomer',
+          'bao' => 'CRM_Stripe_DAO_StripeCustomer',
+          'localizable' => 0,
+          'pseudoconstant' => [
+            'table' => 'civicrm_payment_processor',
+            'keyColumn' => 'id',
+            'labelColumn' => 'name',
+          ],
+          'add' => NULL,
+        ],
+      ];
+      CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
+    }
+    return Civi::$statics[__CLASS__]['fields'];
+  }
+
+  /**
+   * Return a mapping from field-name to the corresponding key (as used in fields()).
+   *
+   * @return array
+   *   Array(string $name => string $uniqueName).
+   */
+  public static function &fieldKeys() {
+    if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) {
+      Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields()));
+    }
+    return Civi::$statics[__CLASS__]['fieldKeys'];
+  }
+
+  /**
+   * Returns the names of this table
+   *
+   * @return string
+   */
+  public static function getTableName() {
+    return self::$_tableName;
+  }
+
+  /**
+   * Returns if this table needs to be logged
+   *
+   * @return bool
+   */
+  public function getLog() {
+    return self::$_log;
+  }
+
+  /**
+   * Returns the list of fields that can be imported
+   *
+   * @param bool $prefix
+   *
+   * @return array
+   */
+  public static function &import($prefix = FALSE) {
+    $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'stripe_customers', $prefix, []);
+    return $r;
+  }
+
+  /**
+   * Returns the list of fields that can be exported
+   *
+   * @param bool $prefix
+   *
+   * @return array
+   */
+  public static function &export($prefix = FALSE) {
+    $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'stripe_customers', $prefix, []);
+    return $r;
+  }
+
+  /**
+   * Returns the list of indices
+   *
+   * @param bool $localize
+   *
+   * @return array
+   */
+  public static function indices($localize = TRUE) {
+    $indices = [
+      'id' => [
+        'name' => 'id',
+        'field' => [
+          0 => 'id',
+        ],
+        'localizable' => FALSE,
+        'unique' => TRUE,
+        'sig' => 'civicrm_stripe_customers::1::id',
+      ],
+    ];
+    return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
+  }
+
+}
diff --git a/CRM/Stripe/DAO/StripePaymentintent.php b/CRM/Stripe/DAO/StripePaymentintent.php
index db987624c75e202712b3e6eaa04fa7e7a4c5daf1..b24c4a078f789fcdedfddce034c46a8f9ce9a9c8 100644
--- a/CRM/Stripe/DAO/StripePaymentintent.php
+++ b/CRM/Stripe/DAO/StripePaymentintent.php
@@ -6,7 +6,7 @@
  *
  * Generated from com.drastikbydesign.stripe/xml/schema/CRM/Stripe/StripePaymentintent.xml
  * DO NOT EDIT.  Generated by CRM_Core_CodeGen
- * (GenCodeChecksum:879fe95dcb092ea9b7a6e7df9ed17657)
+ * (GenCodeChecksum:50c4ef9341699c0242005eede56e04d8)
  */
 use CRM_Stripe_ExtensionUtil as E;
 
@@ -34,28 +34,36 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
   /**
    * Unique ID
    *
-   * @var int
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
    */
   public $id;
 
   /**
    * The Stripe PaymentIntent/SetupIntent/PaymentMethod ID
    *
-   * @var string
+   * @var string|null
+   *   (SQL type: varchar(255))
+   *   Note that values will be retrieved from the database as a string.
    */
   public $stripe_intent_id;
 
   /**
    * FK ID from civicrm_contribution
    *
-   * @var int
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
    */
   public $contribution_id;
 
   /**
    * Foreign key to civicrm_payment_processor.id
    *
-   * @var int
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
    */
   public $payment_processor_id;
 
@@ -63,6 +71,8 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
    * Description of this paymentIntent
    *
    * @var string
+   *   (SQL type: varchar(255))
+   *   Note that values will be retrieved from the database as a string.
    */
   public $description;
 
@@ -70,6 +80,8 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
    * The status of the paymentIntent
    *
    * @var string
+   *   (SQL type: varchar(25))
+   *   Note that values will be retrieved from the database as a string.
    */
   public $status;
 
@@ -77,20 +89,26 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
    * An identifier that we can use in CiviCRM to find the paymentIntent if we do not have the ID (eg. session key)
    *
    * @var string
+   *   (SQL type: varchar(255))
+   *   Note that values will be retrieved from the database as a string.
    */
   public $identifier;
 
   /**
    * FK to Contact
    *
-   * @var int
+   * @var int|string|null
+   *   (SQL type: int unsigned)
+   *   Note that values will be retrieved from the database as a string.
    */
   public $contact_id;
 
   /**
    * When was paymentIntent created
    *
-   * @var timestamp
+   * @var string|null
+   *   (SQL type: timestamp)
+   *   Note that values will be retrieved from the database as a string.
    */
   public $created_date;
 
@@ -98,6 +116,8 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
    * Flags associated with this PaymentIntent (NC=no contributionID when doPayment called)
    *
    * @var string
+   *   (SQL type: varchar(100))
+   *   Note that values will be retrieved from the database as a string.
    */
   public $flags;
 
@@ -105,6 +125,8 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
    * HTTP referrer of this paymentIntent
    *
    * @var string
+   *   (SQL type: varchar(255))
+   *   Note that values will be retrieved from the database as a string.
    */
   public $referrer;
 
@@ -112,6 +134,8 @@ class CRM_Stripe_DAO_StripePaymentintent extends CRM_Core_DAO {
    * Extra data collected to help with diagnostics (such as email, name)
    *
    * @var string
+   *   (SQL type: varchar(255))
+   *   Note that values will be retrieved from the database as a string.
    */
   public $extra_data;
 
diff --git a/Civi/Api4/StripeCustomer.php b/Civi/Api4/StripeCustomer.php
new file mode 100644
index 0000000000000000000000000000000000000000..774ed13bd62b56bba397134ad78977b9737c53cc
--- /dev/null
+++ b/Civi/Api4/StripeCustomer.php
@@ -0,0 +1,13 @@
+<?php
+namespace Civi\Api4;
+
+/**
+ * StripeCustomer entity.
+ *
+ * Provided by the Stripe Payment Processor extension.
+ *
+ * @package Civi\Api4
+ */
+class StripeCustomer extends Generic\DAOEntity {
+
+}
diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql
index 3e94c264003c4c830a9269e70c218b4fa1d42b9e..38771c61c87613b9d5254ea1255feb051ba3aed9 100644
--- a/sql/auto_uninstall.sql
+++ b/sql/auto_uninstall.sql
@@ -18,4 +18,4 @@ SET FOREIGN_KEY_CHECKS=0;
 DROP TABLE IF EXISTS `civicrm_stripe_paymentintent`;
 DROP TABLE IF EXISTS `civicrm_stripe_customers`;
 
-SET FOREIGN_KEY_CHECKS=1;
+SET FOREIGN_KEY_CHECKS=1;
\ No newline at end of file
diff --git a/sql/customers_install.sql b/sql/customers_install.sql
index 5cae99d4587decadce0046c4142c311fe58e5b7d..498f6602716d1e9e606b519ea519b2b7fc26260c 100644
--- a/sql/customers_install.sql
+++ b/sql/customers_install.sql
@@ -1,8 +1,21 @@
-CREATE TABLE IF NOT EXISTS `civicrm_stripe_customers` (
-  `id` varchar(255) DEFAULT NULL,
-  `contact_id` int(10) UNSIGNED DEFAULT NULL COMMENT 'FK ID from civicrm_contact',
-  `processor_id` int(10) DEFAULT NULL COMMENT 'ID from civicrm_payment_processor',
-  UNIQUE KEY `id` (`id`),
-  CONSTRAINT `FK_civicrm_stripe_customers_contact_id` FOREIGN KEY (`contact_id`)
-  REFERENCES `civicrm_contact` (`id`) ON DELETE CASCADE
-) ENGINE=InnoDB;
+-- /*******************************************************
+-- *
+-- * Create new tables
+-- *
+-- *******************************************************/
+
+-- /*******************************************************
+-- *
+-- * civicrm_stripe_customers
+-- *
+-- * Stripe Customers
+-- *
+-- *******************************************************/
+CREATE TABLE `civicrm_stripe_customers` (
+  `id` varchar(255) COMMENT 'Stripe Customer ID',
+  `contact_id` int unsigned COMMENT 'FK to Contact',
+  `processor_id` int unsigned COMMENT 'ID from civicrm_payment_processor',
+  UNIQUE INDEX `id`(id),
+  CONSTRAINT FK_civicrm_stripe_customers_contact_id FOREIGN KEY (`contact_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE
+)
+ENGINE=InnoDB;
diff --git a/stripe.civix.php b/stripe.civix.php
index 0d1c0bd297a45e7d18af6ef8244d1f8291c1002d..a50aefbe377a2114b71faec8fd7049002767e158 100644
--- a/stripe.civix.php
+++ b/stripe.civix.php
@@ -295,6 +295,11 @@ function _stripe_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) {
  */
 function _stripe_civix_civicrm_entityTypes(&$entityTypes) {
   $entityTypes = array_merge($entityTypes, [
+    'CRM_Stripe_DAO_StripeCustomer' => [
+      'name' => 'StripeCustomer',
+      'class' => 'CRM_Stripe_DAO_StripeCustomer',
+      'table' => 'civicrm_stripe_customers',
+    ],
     'CRM_Stripe_DAO_StripePaymentintent' => [
       'name' => 'StripePaymentintent',
       'class' => 'CRM_Stripe_DAO_StripePaymentintent',
diff --git a/tests/phpunit/api/v3/StripeCustomerTest.php b/tests/phpunit/api/v3/StripeCustomerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b24abf49cc83af8a74fe343e34604b68d9e5366
--- /dev/null
+++ b/tests/phpunit/api/v3/StripeCustomerTest.php
@@ -0,0 +1,67 @@
+<?php
+
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * StripeCustomer API Test Case
+ * @group headless
+ */
+class api_v3_StripeCustomerTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+  use \Civi\Test\Api3TestTrait;
+
+  /**
+   * Set up for headless tests.
+   *
+   * Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+   *
+   * See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+   */
+  public function setUpHeadless() {
+    return \Civi\Test::headless()
+      ->installMe(__DIR__)
+      ->apply();
+  }
+
+  /**
+   * The setup() method is executed before the test is executed (optional).
+   */
+  public function setUp() {
+    $table = CRM_Core_DAO_AllCoreTables::getTableForEntityName('StripeCustomer');
+    $this->assertTrue($table && CRM_Core_DAO::checkTableExists($table), 'There was a problem with extension installation. Table for ' . 'StripeCustomer' . ' not found.');
+    parent::setUp();
+  }
+
+  /**
+   * The tearDown() method is executed after the test was executed (optional)
+   * This can be used for cleanup.
+   */
+  public function tearDown() {
+    parent::tearDown();
+  }
+
+  /**
+   * Simple example test case.
+   *
+   * Note how the function name begins with the word "test".
+   */
+  public function testCreateGetDelete() {
+    // Boilerplate entity has one data field -- 'contact_id'.
+    // Put some data in, read it back out, and delete it.
+
+    $created = $this->callAPISuccess('StripeCustomer', 'create', [
+      'contact_id' => 1,
+    ]);
+    $this->assertTrue(is_numeric($created['id']));
+
+    $get = $this->callAPISuccess('StripeCustomer', 'get', []);
+    $this->assertEquals(1, $get['count']);
+    $this->assertEquals(1, $get['values'][$created['id']]['contact_id']);
+
+    $this->callAPISuccess('StripeCustomer', 'delete', [
+      'id' => $created['id'],
+    ]);
+  }
+
+}
diff --git a/xml/schema/CRM/Stripe/StripeCustomer.entityType.php b/xml/schema/CRM/Stripe/StripeCustomer.entityType.php
new file mode 100644
index 0000000000000000000000000000000000000000..923ead6499b395dec66e005ca23d84e138ba76eb
--- /dev/null
+++ b/xml/schema/CRM/Stripe/StripeCustomer.entityType.php
@@ -0,0 +1,10 @@
+<?php
+// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at:
+// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+return [
+  [
+    'name' => 'StripeCustomer',
+    'class' => 'CRM_Stripe_DAO_StripeCustomer',
+    'table' => 'civicrm_stripe_customers',
+  ],
+];
diff --git a/xml/schema/CRM/Stripe/StripeCustomer.xml b/xml/schema/CRM/Stripe/StripeCustomer.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d47501fc78f405d5eb4ede8ca5172fdd9f51b75d
--- /dev/null
+++ b/xml/schema/CRM/Stripe/StripeCustomer.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="iso-8859-1" ?>
+
+<table>
+  <base>CRM/Stripe</base>
+  <class>StripeCustomer</class>
+  <name>civicrm_stripe_customers</name>
+  <comment>Stripe Customers</comment>
+  <log>true</log>
+
+  <field>
+    <name>id</name>
+    <title>Stripe Customer ID</title>
+    <type>varchar</type>
+    <length>255</length>
+    <comment>The Stripe Customer ID</comment>
+  </field>
+  <index>
+    <name>id</name>
+    <fieldName>id</fieldName>
+    <unique>true</unique>
+  </index>
+
+  <field>
+    <name>contact_id</name>
+    <type>int unsigned</type>
+    <comment>FK to Contact</comment>
+  </field>
+  <foreignKey>
+    <name>contact_id</name>
+    <table>civicrm_contact</table>
+    <key>id</key>
+    <onDelete>CASCADE</onDelete>
+  </foreignKey>
+
+  <field>
+    <name>processor_id</name>
+    <title>Payment Processor ID</title>
+    <type>int unsigned</type>
+    <comment>Foreign key to civicrm_payment_processor.id</comment>
+    <pseudoconstant>
+      <table>civicrm_payment_processor</table>
+      <keyColumn>id</keyColumn>
+      <labelColumn>name</labelColumn>
+    </pseudoconstant>
+  </field>
+  <!-- <foreignKey>
+    <name>processor_id</name>
+    <table>civicrm_payment_processor</table>
+    <key>id</key>
+    <onDelete>SET NULL</onDelete>
+  </foreignKey> -->
+
+  
+</table>