Search code examples
jsongoogle-apps-scriptpoststripe-payments

Post Nested Objects to Stripe via Google Apps Script Web App


Description

I'm using a Google Apps Script Web App to listen for webhook events from Stripe as well as to post data back. I can post values to attributes that are at the top level without issues, e.g.:

const data = await postCustomer(customerId, {
        name: 'Name',
        email: '[email protected]'
    }
  })
async function postCustomer(id, args) {
  const data = await postStripeData(id, args, ['customers'])
  return data
}

async function postStripeData(id, args, path1, path2) {
  const fullPath = makeFullPath(id, path1, path2)
  const data = await post(getStripeApiUrl() + fullPath + '?key=' + getKey(), args)
  return data
}

async function post(url, args) {
  const response = await UrlFetchApp.fetch(url, {
    'method': 'post',
    'payload': args
  })
    if (await response.getResponseCode() === 200) {
      const data = await JSON.parse(response)
    return data
  } else {
    throw new Error("I couldn't post to this url: " + url)
  }
}

Problem

However, I can't post objects within keys (I used a real location in my post request):

const data = await postCustomer(customerId, {
    address: {
      country: 'US',
      state: 'CA',
      city: 'City',
      postal_code: '00000',
      line1: 'Line 1',
      line2: 'Line 2'
    }
  })

The error I get is that address is an invalid object, and it appears in my Stripe logs like this:

{
"address": 
"{line2=Line 2, state=CA, country=US, postal_code=00000, line1=Line 1, city=City}",
}

The documentation displays similarly, though it doesn't show an example for GAS. Here's the Node.js example (it didn't make a difference whether or not I include trailing commas in the payload object and the nested object or whether I quoted the key names):

const stripe = require('stripe')('sk_test_51PoHIuFYLeju0XxwXR4z3EHW3oaSqtTlD2Kr2xvUPRzhORHsjssb35rLfw1kbdQsVYdkHiYZ7vmKgWrXic30Y2XN00bvs1FZaO');
const customer = await stripe.customers.update(
  'cus_NffrFeUfNV2Hib',
  {
    metadata: {
      order_id: '6735',
    },
  }
);

What I've Tried

I tried using JSON.stringify() on address. I get the same error message, and it shows in the log like this:

{
"address": "{"country":"US"}"
}

I also tried using JSON.stringify() on the entire payload in post(). In this case, I get a response of 200 and can successfully return the response, but the customer doesn't get updated.

I also tried sending the address as a Blob, but I don't really know how to use those. The error was that it got the wrong format (application/x-www-form-urlencoded is what is expected, and this appears to be what UrlFetchApp.fetch() defaults to).


Solution

  • When I saw your provided URL, I found the following curl command.

    curl https://api.stripe.com/v1/customers/cus_NffrFeUfNV2Hib \
      -u "sk_test_09l3shTSTKHYCzzZZsiLl2vA:" \
      -d "metadata[order_id]"=6735
    

    In the case of the property including objects, it seems that "metadata[order_id]"=6735 is used. In the case of

    {
      address: {
        country: 'US',
        state: 'CA',
        city: 'City',
        postal_code: '00000',
        line1: 'Line 1',
        line2: 'Line 2'
      }
    }
    

    how about the following modification?

    From:

    async function postCustomer(id, args) {
      const data = await postStripeData(id, args, ['customers'])
      return data
    }
    
    async function postStripeData(id, args, path1, path2) {
      const fullPath = makeFullPath(id, path1, path2)
      const data = await post(getStripeApiUrl() + fullPath + '?key=' + getKey(), args)
      return data
    }
    
    async function post(url, args) {
      const response = await UrlFetchApp.fetch(url, {
        'method': 'post',
        'payload': args
      })
        if (await response.getResponseCode() === 200) {
          const data = await JSON.parse(response)
        return data
      } else {
        throw new Error("I couldn't post to this url: " + url)
      }
    }
    

    To:

    In the current stage, Google Apps Script works with synchronous processes.

    function postCustomer(id, args) {
      const data = postStripeData(id, args, ['customers'])
      return data
    }
    
    function postStripeData(id, args, path1, path2) {
      const fullPath = makeFullPath(id, path1, path2)
      const data = post(getStripeApiUrl() + fullPath + '?key=' + getKey(), args)
      return data
    }
    
    function post(url, args) {
    
      // --- I added the below script.
      args = Object.fromEntries(Object.entries(args).flatMap(([k1, v1]) =>
        typeof v1 == "object" ? Object.entries(v1).map(([k2, v2]) => [`${k1}[${k2}]`, v2]) : [[k1, v1]]
      ));
      // ---
    
      const response = UrlFetchApp.fetch(url, {
        'method': 'post',
        'payload': args
      })
      if (response.getResponseCode() === 200) {
        const data = JSON.parse(response)
        return data
      } else {
        throw new Error("I couldn't post to this url: " + url)
      }
    }
    

    By this, when the following script is run,

    const data = postCustomer(customerId, {
      address: {
        country: 'US',
        state: 'CA',
        city: 'City',
        postal_code: '00000',
        line1: 'Line 1',
        line2: 'Line 2'
      }
    });
    

    The payload is as follows.

    {
      "address[country]": "US",
      "address[state]": "CA",
      "address[city]": "City",
      "address[postal_code]": "00000",
      "address[line1]": "Line 1",
      "address[line2]": "Line 2"
    }
    

    Note:

    • I cannot know the relationship between your question and Web Apps. But, if you use Web Apps with doGet and doPost, when you modify the script, please reflect the latest script to the Web Apps. Please be careful about this.

    Reference: