Stripe Connect module API explorer

/api/user/connect/create-person (POST)

Account information like email addresses is generated with faker-js it is not real user information.

await global.api.user.connect.CreatePerson.post(req)

Returns object

{
  "personid": "person_1LEBsARbTvuVhFnBZw9F7lQj",
  "object": "person",
  "accountid": "acct_2eded9e851ef76a5",
  "stripeid": "acct_1LEBs5RbTvuVhFnB",
  "tokenUpdate": null,
  "stripeObject": {
    "id": "person_1LEBsARbTvuVhFnBZw9F7lQj",
    "object": "person",
    "account": "acct_1LEBs5RbTvuVhFnB",
    "address": {
      "city": null,
      "country": "GB",
      "line1": null,
      "line2": null,
      "postal_code": null,
      "state": null
    },
    "created": 1656074670,
    "dob": {
      "day": null,
      "month": null,
      "year": null
    },
    "first_name": null,
    "future_requirements": {
      "alternatives": [],
      "currently_due": [],
      "errors": [],
      "eventually_due": [],
      "past_due": [],
      "pending_verification": []
    },
    "last_name": null,
    "metadata": {},
    "relationship": {
      "director": false,
      "executive": false,
      "owner": true,
      "percent_ownership": 0.1,
      "representative": false,
      "title": "Chairperson"
    },
    "requirements": {
      "alternatives": [],
      "currently_due": [
        "address.city",
        "address.line1",
        "address.postal_code",
        "dob.day",
        "dob.month",
        "dob.year",
        "email",
        "first_name",
        "last_name"
      ],
      "errors": [],
      "eventually_due": [
        "address.city",
        "address.line1",
        "address.postal_code",
        "dob.day",
        "dob.month",
        "dob.year",
        "email",
        "first_name",
        "last_name"
      ],
      "past_due": [
        "address.city",
        "address.line1",
        "address.postal_code",
        "dob.day",
        "dob.month",
        "dob.year",
        "email",
        "first_name",
        "last_name"
      ],
      "pending_verification": []
    },
    "verification": {
      "additional_document": {
        "back": null,
        "details": null,
        "details_code": null,
        "front": null
      },
      "details": null,
      "details_code": null,
      "document": {
        "back": null,
        "details": null,
        "details_code": null,
        "front": null
      },
      "status": "unverified"
    }
  },
  "appid": "tests_1656074644",
  "createdAt": "2022-06-24T12:44:32.022Z",
  "updatedAt": "2022-06-24T12:44:32.022Z"
}

Receives

API routes may receive parameters from the URL and POST supporting simple and multipart:

Field Value Required Type
relationship_director string optional POST
relationship_executive string configurable as required POST
relationship_owner string optional POST
relationship_percent_ownership string required POST
relationship_representative string optional POST
relationship_title string required POST

Exceptions

These exceptions are thrown (NodeJS) or returned as JSON (HTTP) if you provide incorrect data or do not meet the requirements:

Exception Circumstances
invalid-account ineligible accessing account
invalid-relationship_percent_ownership missing posted relationship.percent_ownership
invalid posted relationship.percent_ownership
invalid-relationship_title missing posted relationship.title
invalid posted relationship_title
invalid-stripe-account ineligible stripe account for individuals
ineligible stripe account does not require directors
ineligible stripe account does not require owners
ineligible stripe account does not require executives
invalid-stripeid missing querystring stripeid
invalid querystring stripeid

NodeJS source (view on github)

const connect = require('../../../../../index.js')
const stripeCache = require('../../../../stripe-cache.js')

module.exports = {
  post: async (req) => {
    if (!req.query || !req.query.stripeid) {
      throw new Error('invalid-stripeid')
    }
    req.body = req.body || {}
    const stripeAccount = await global.api.user.connect.StripeAccount.get(req)
    if (!stripeAccount) {
      throw new Error('invalid-stripeid')
    }
    if (stripeAccount.stripeObject.business_type !== 'company') {
      throw new Error('invalid-stripe-account')
    }
    if (!req.body.relationship_representative &&
        !req.body.relationship_director &&
        !req.body.relationship_owner &&
        !req.body.relationship_executive) {
      throw new Error('invalid-selection')
    }
    // TODO: the 5000 character limit is from Stripe
    // they'll probably change it so monitor this
    if (!req.body.relationship_title ||
        !req.body.relationship_title.length ||
        req.body.relationship_title.length > 5000) {
      throw new Error('invalid-relationship_title')
    }
    if (!req.body.relationship_percent_ownership) {
      throw new Error('invalid-relationship_percent_ownership')
    }
    let percent
    try {
      percent = parseFloat(req.body.relationship_percent_ownership, 10)
    } catch (s) {
      throw new Error('invalid-relationship_percent_ownership')
    }
    // TODO: 0% ownership throws an error on Stripe if the person is not 'owner=true'
    if ((!percent && percent !== 0) || percent > 100 || percent < 0) {
      throw new Error('invalid-relationship_percent_ownership')
    }
    if (percent === 0) {
      if (req.body.relationship_owner) {
        throw new Error('invalid-relationship_percent_ownership')
      }
      delete (req.body.relationship_percent_ownership)
    }
    const personInfo = {
      relationship: {
        title: req.body.relationship_title
      }
    }
    if (req.body.relationship_percent_ownership) {
      personInfo.relationship.percent_ownership = req.body.relationship_percent_ownership
    }
    if (req.body.relationship_representative) {
      personInfo.relationship.representative = true
    }
    if (req.body.relationship_executive === 'true') {
      if (!personInfo.relationship.representative && !stripeAccount.requiresExecutives) {
        throw new Error('invalid-stripe-account')
      }
      personInfo.relationship.executive = req.body.relationship_executive || false
    }
    if (req.body.relationship_director === 'true') {
      if (!stripeAccount.requiresDirectors) {
        throw new Error('invalid-stripe-account')
      }
      personInfo.relationship.director = req.body.relationship_director || false
    }
    if (req.body.relationship_owner === 'true') {
      if (!stripeAccount.requiresOwners) {
        throw new Error('invalid-stripe-account')
      }
      personInfo.relationship.owner = req.body.relationship_owner || false
    }
    try {
      const person = await stripeCache.execute('accounts', 'createPerson', req.query.stripeid, personInfo, req.stripeKey)
      await connect.Storage.Person.create({
        appid: req.appid || global.appid,
        personid: person.id,
        accountid: req.account.accountid,
        stripeid: stripeAccount.stripeid,
        stripeObject: person
      })
      req.query.personid = person.id
      return global.api.user.connect.Person.get(req)
    } catch (error) {
      if (error.message.startsWith('invalid-')) {
        throw new Error(error.message.split('.').join('_'))
      }
      throw error
    }
  }
}

Test source (view on github)

/* eslint-env mocha */
const assert = require('assert')
const TestHelper = require('../../../../../test-helper.js')
const DashboardTestHelper = require('@layeredapps/dashboard/test-helper.js')

describe('/api/user/connect/create-person', function () {
  before(TestHelper.disableMetrics)
  after(TestHelper.enableMetrics)
  let cachedResponses
  async function bundledData (retryNumber) {
    if (retryNumber > 0) {
      cachedResponses = {}
    }
    if (cachedResponses && cachedResponses.finished) {
      return
    }
    cachedResponses = {}
    await DashboardTestHelper.setupBeforeEach()
    await TestHelper.setupBeforeEach()
    const user = await TestHelper.createUser()
    await TestHelper.createStripeAccount(user, {
      country: 'US',
      business_type: 'individual'
    })
    // individual account
    let req = TestHelper.createRequest(`/api/user/connect/create-person?stripeid=${user.stripeAccount.stripeid}`)
    req.account = user.account
    req.session = user.session
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.individualAccount = error.message
    }
    // does not require owner
    await TestHelper.createStripeAccount(user, {
      country: 'CA',
      business_type: 'company'
    })
    req = TestHelper.createRequest(`/api/user/connect/create-person?stripeid=${user.stripeAccount.stripeid}`)
    req.account = user.account
    req.session = user.session
    req.body = {
      relationship_owner: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: '0.1'
    }
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.ownersNotRequired = error.message
    }
    // executives not required
    req.body = {
      relationship_executive: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: '0.1'
    }
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.executivesNotRequired = error.message
    }
    // invalid account
    const user2 = await TestHelper.createUser()
    req = TestHelper.createRequest(`/api/user/connect/create-person?stripeid=${user.stripeAccount.stripeid}`)
    req.account = user2.account
    req.session = user2.session
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.invalidAccount = error.message
    }
    // does not require director
    await TestHelper.createStripeAccount(user, {
      country: 'US',
      business_type: 'company'
    })
    req = TestHelper.createRequest(`/api/user/connect/create-person?stripeid=${user.stripeAccount.stripeid}`)
    req.account = user.account
    req.session = user.session
    req.body = {
      relationship_director: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: '0.1'
    }
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.directorsNotRequired = error.message
    }
    // missing percent owned
    await TestHelper.createStripeAccount(user, {
      country: 'AT',
      business_type: 'company'
    })
    req = TestHelper.createRequest(`/api/user/connect/create-person?stripeid=${user.stripeAccount.stripeid}`)
    req.account = user.account
    req.session = user.session
    req.body = {
      relationship_representative: 'true',
      relationship_executive: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: ''
    }
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.missingPercentOwned = error.message
    }
    // invalid percent owned
    req.body = {
      relationship_representative: 'true',
      relationship_executive: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: 'invalid'
    }
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.invalidPercentOwned = error.message
    }
    // missing relationship title
    req.body = {
      relationship_representative: 'true',
      relationship_executive: 'true',
      relationship_title: '',
      relationship_percent_ownership: '0.1'
    }
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.missingRelationshipTitle = error.message
    }
    // invalid relationship title
    while (req.body.relationship_title.length < 5001) {
      req.body.relationship_title += '-'
    }
    try {
      await req.post(req)
    } catch (error) {
      cachedResponses.invalidRelationshipTitle = error.message
    }
    // posted executive
    req.body = {
      relationship_representative: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: '0.1'
    }
    cachedResponses.representative = await req.post()
    // posted executive
    req.body = {
      relationship_executive: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: '0.1'
    }
    cachedResponses.executive = await req.post()
    // posted director
    req.body = {
      relationship_director: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: '0.1'
    }
    cachedResponses.director = await req.post()
    // posted owner
    await TestHelper.createStripeAccount(user, {
      country: 'GB',
      business_type: 'company'
    })
    req = TestHelper.createRequest(`/api/user/connect/create-person?stripeid=${user.stripeAccount.stripeid}`)
    req.account = user.account
    req.session = user.session
    req.filename = __filename
    req.saveResponse = true
    req.body = {
      relationship_owner: 'true',
      relationship_title: 'Chairperson',
      relationship_percent_ownership: '0.1'
    }
    cachedResponses.owner = await req.post()
    cachedResponses.finished = true
  }

  describe('exceptions', () => {
    describe('invalid-stripeid', () => {
      it('missing querystring stripeid', async () => {
        const user = await TestHelper.createUser()
        const req = TestHelper.createRequest('/api/user/connect/create-person')
        req.account = user.account
        req.session = user.session
        let errorMessage
        try {
          await req.post(req)
        } catch (error) {
          errorMessage = error.message
        }
        assert.strictEqual(errorMessage, 'invalid-stripeid')
      })

      it('invalid querystring stripeid', async () => {
        const user = await TestHelper.createUser()
        const req = TestHelper.createRequest('/api/user/connect/create-person?stripeid=invalid')
        req.account = user.account
        req.session = user.session
        let errorMessage
        try {
          await req.post(req)
        } catch (error) {
          errorMessage = error.message
        }
        assert.strictEqual(errorMessage, 'invalid-stripeid')
      })
    })

    describe('invalid-stripe-account', () => {
      it('ineligible stripe account for individuals', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.individualAccount
        assert.strictEqual(errorMessage, 'invalid-stripe-account')
      })

      it('ineligible stripe account does not require directors', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.directorsNotRequired
        assert.strictEqual(errorMessage, 'invalid-stripe-account')
      })

      it('ineligible stripe account does not require owners', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.ownersNotRequired
        assert.strictEqual(errorMessage, 'invalid-stripe-account')
      })

      it('ineligible stripe account does not require executives', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.executivesNotRequired
        assert.strictEqual(errorMessage, 'invalid-stripe-account')
      })
    })

    describe('invalid-account', () => {
      it('ineligible accessing account', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalidAccount
        assert.strictEqual(errorMessage, 'invalid-account')
      })
    })

    describe('invalid-relationship_percent_ownership', () => {
      it('missing posted relationship.percent_ownership', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.missingPercentOwned
        assert.strictEqual(errorMessage, 'invalid-relationship_percent_ownership')
      })

      it('invalid posted relationship.percent_ownership', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalidPercentOwned
        assert.strictEqual(errorMessage, 'invalid-relationship_percent_ownership')
      })
    })

    describe('invalid-relationship_title', () => {
      it('missing posted relationship.title', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.missingRelationshipTitle
        assert.strictEqual(errorMessage, 'invalid-relationship_title')
      })

      it('invalid posted relationship_title', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalidRelationshipTitle
        assert.strictEqual(errorMessage, 'invalid-relationship_title')
      })
    })
  })

  describe('receives', () => {
    it('optional posted relationship_representative', async function () {
      await bundledData(this.test.currentRetry())
      const person = cachedResponses.representative
      assert.strictEqual(person.stripeObject.relationship.representative, true)
    })

    it('optionally-required posted relationship_executive', async function () {
      await bundledData(this.test.currentRetry())
      const person = cachedResponses.executive
      assert.strictEqual(person.stripeObject.relationship.executive, true)
    })

    it('optional posted relationship_director', async function () {
      await bundledData(this.test.currentRetry())
      const person = cachedResponses.director
      assert.strictEqual(person.stripeObject.relationship.director, true)
    })

    it('optional posted relationship_owner', async function () {
      await bundledData(this.test.currentRetry())
      const person = cachedResponses.owner
      assert.strictEqual(person.stripeObject.relationship.owner, true)
    })

    it('required posted relationship_percent_ownership', async function () {
      await bundledData(this.test.currentRetry())
      const person = cachedResponses.owner
      assert.strictEqual(person.stripeObject.relationship.percent_ownership, 0.1)
    })

    it('required posted relationship_title', async () => {
      const person = cachedResponses.owner
      await bundledData()
      assert.strictEqual(person.stripeObject.relationship.title, 'Chairperson')
    })
  })

  describe('returns', () => {
    it('object', async function () {
      await bundledData(this.test.currentRetry())
      const person = cachedResponses.owner
      assert.strictEqual(person.object, 'person')
    })
  })
})