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@example.com'
    }
  })
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:

    Reference: