Generating QR Codes with the Flowcode API

March 3rd, 2022 · 28 minute read

We built Flowcode to be a gateway between the real and virtual worlds — a way of seamlessly connecting an offline audience to online experiences. The Flowcode API doesn’t just streamline this process, it supercharges it. In a few lines of code, engineers can programmatically create thousands of QR codes, assign behaviors to them using pre-built smart rules, and organize them into campaigns. With the right support, the API enables developers to solve some of the most challenging problems facing modern businesses: generating unique identifiers, providing information to customers, and tracking user engagement across every offline channel.

In this article, we’ll go through how to generate Flowcodes in a set of clear, simple steps. To help us, we’ll use one of the most common uses for our API: tracking offline marketing channels.

GET STARTED WITH THE FLOWCODE API


Our QR codes solve this problem by connecting offline marketing materials to online stores and websites. They can be used to track offline engagement at an extremely granular level, allowing companies to prioritize the advertisements and products that customers love and to rethink those that they don’t.
The analytics for a single Flowcode. Our software is completely GDPR and CCPA compliant, so both companies and customers can be confident about using it without violating privacy requirements.

If you’re going to be generating Flowcodes for use in marketing, you probably need a dataset that looks something like this, containing ad ids and their associated campaigns. In this guide, we’ll create campaigns for each of the unique xyz_campaign_id listings, and unique Flowcodes for each ad.




* This example advertisement dataset was taken from the open-source UCI Machine Learning Repository

Getting Started: APIs and the Flowcode Client ID

The first thing that you’ll need to use the API is a client id. This is a unique string that the Flowcode team provides to all Premium customers, and you’ll need it to access any of the API endpoints. It should look something like "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", and will be composed of a combination of letters and numbers. If you can’t find it, reach out to [email protected] and we’ll help!

The code in this guide is written in Python for the sake of readability and makes use of the pandas library for dealing with data. If you’ve never used it before, you can read about it here . This guide also assumes you have some basic knowledge about how APIs work. If you need a refresher, Hubspot has a great article about them.

GET STARTED WITH THE FLOWCODE API

Creating Campaigns with the API

Right, let’s get started!

Flowcode’s API organises QR codes through campaigns, each of which is associated with a set of QR codes. Depending on your requirements, campaigns might represent anything from geographic regions, to marketing initiatives, to time periods. For our example, campaigns represent issues of a magazine that we’re generating ads for.

The first step in generating Flowcodes is creating a set of campaigns. For our ad dataset, we can see after some analysis that we have three xyz campaigns: 916, 936 and 1178.

 

import pandas as pd # pandas will help us process our dataset
import requests # requests is used to send and receive information from the API
ad_data = pd.read_csv("./ads.csv") # importing the data
ad_data.head() # checking out its structure

# cutting all unnecessary columns
ad_data = ad_data.loc[:, ['ad_id', "xyz_campaign_id"]] print(ad_data['xyz_campaign_id'].unique()) # [916, 936, 1178]

 

We’ll create Flowcode campaigns for each of these, using the batch/bulk-campaign endpoint. This requires four pieces of information:

  • name: the unique name of the campaign, which we’ll use to identify it; this requires a string

  • display_name: a more human-readable version of the campaign’s name used when displaying information about it; requires a string

  • client_id: the unique password that allows you to use the API, a string of the form "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"

  • reserved_urls_unique: whether each URL in the campaign is unique (i.e. has only one Flowcode pointing to it): requires True or False 

# getting all campaign names from the dataset
campaigns = ad_data['xyz_campaign_id'].unique()

# setting the endpoint for our API request
campaign_url = "https://api.flowcode.com/v2/flowcode/batch/bulk-campaign" 

 

# creating a new campaign for each campaign in the dataset
for campaign in campaigns: 

     data = { "name": f"{campaign}"

              "display_name": f"{campaign} display"

              "client_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",       

              "reserved_urls_unique": False, } 

     try

        response = requests.post(campaign_url, data)    

        response.raise_for_status() 

     except requests.exceptions.HTTPError as err: 

         if err.response.status_code == 409

              print(f"Campaign {campaign} already exists! Skipping creation"

         else

             raise err

 

The try/except clauses here will raise an exception if the response object is invalid (a 400 error), except if we get a 409 error, which indicates we’ve already created the campaign.

There’s also an optional image_types argument that we won’t discuss in this article, which allows you to dictate what the Flowcodes associated with the campaign will look like, including their colour scheme, size, resolution and graphics. For more detailed information, check out our API documentation .

GET STARTED WITH THE FLOWCODE API

Checking Campaigns with the API

After creating campaigns, we often want to check their configuration, which we can do by calling a GET request to the same endpoint. This request requires only two pieces of information:

  • client_id: the unique password that allows you to use the API, a string of the form "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"

  • name: the unique name of the campaign, which must be a string

# sending requests to check out each of our campaigns
for campaign in campaigns:
    params = {
      'client_id': "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
      'name': f"{campaign}" }
    try:
      response = requests.get(campaign_url, params)     
      response.raise_for_status() print(response.text)
    except requests.exceptions.HTTPError as err:
      raise err

This will return an object containing all of the information about each specified campaign that’s similar to the one below. 

 

Generating Simple Flowcodes for Each Campaign Using the API

Now that we’ve created campaigns for our dataset, we can generate Flowcodes that are assigned to each one. Our API provides lots of functionality for controlling the codes that you generate, including features like redirecting users based on the time of day, day of the week, and device type. Before getting into that, though, let’s look at the straightforward way to create Flowcodes.

When we create Flowcodes for each campaign, we use the batch/bulk endpoint, which (for this simple example) requires three pieces of information:

  • client_id: the unique password that allows you to use the API, a string of the form "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"

  • name: the unique name of the campaign, which must be a string

  • urls: a list of URL objects (dictionaries in Python)

Separating Ads for Each Campaign

campaign_data = []
for campaign in campaigns:
# get all rows where the campaign id is equal to our active campaign
campaign_data.append(ad_data.loc[ad_data["xyz_campaign_id"] == campaign])

If you need help with Dataframe subsetting in Python, here’s an excellent overview

This turns campaign_data into a list containing three Dataframes: one for each of our campaigns.

Generating URL Objects for Every QR Code

Next, let’s generate a list of url objects for each campaign to send to the endpoint. Each url object requires three pieces of information:

  • id: the unique ID of the Flowcode, which should be a string

  • url_type: the type of URL (e.g. “URL”)

  • url: the URL address to which the Flowcode will redirect users

We can use the id of each advertisement to set the Flowcode id, which will be useful later for connecting analytics to specific ads.

flowcodes = [] # initialise an empty list to store our url data

for campaign in campaign_data:
    campaign_list = []
    for index, row in campaign.iterrows():
        campaign_list.append(
            { "id": f"{row.ad_id}",
              "url_type": "URL",
              "url": f"http://www.flowcode.com?id={row.ad_id}"
            })
    flowcodes.append(campaign_list)

We now have three lists of url objects, all stored inside the Flowcodes list.

You might have noticed that we’re programmatically creating URLs of the form http://www.flowcode.com?id={row.ad_id}. This is just one way of creating reactive Flowcodes, and passes an id argument to the browser which can be used to customize the webpage based on the specific Flowcode that was scanned. If the website isn’t designed to accept such an argument, it’ll helpfully just return the normal page (http://www.flowcode.com). For other use-cases like tracking offline engagement, you might just have every code pointing to the same webpage.

After running the code above, each list in Flowcodes has become a list of objects similar to the one below:

 


Sending the QR Code Request

Now, we have to send three POST requests to the batch/bulk endpoint: one for each campaign.

 

codes_url = "https://api.flowcode.com/v2/flowcode/batch/bulk"
responses = {} # create a dictionary for storing campaigns and responses

# send a request for each campaign in "flowcodes"
for index, value in enumerate(flowcodes):
    if not value: # deals with errors caused by empty campaigns
        pass
    else:
        codes_data = {
            "client_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
            "campaign_name": f"{campaigns[index]}",
            "urls": flowcodes[index] }
        try:
        # send the data as a json to support the formatting of "urls"  
            flowcode_response = requests.post(codes_url, json=codes_data)  
            flowcode_response.raise_for_status() 
            responses[f'{campaigns[index]}'] = flowcode_response 
            print("Flowcodes Created")
        except requests.exceptions.HTTPError as err:
            if err.response.status_code == 409:
                print(f"Some URLS in Campaign {campaigns[index]} already exist! Skipping creation")
            else:
                raise err

Accessing the QR Codes from The Response Object

These responses include lots of information about the Flowcodes that we generated, including their id, the URL that they redirect to (redirect_value), and, most importantly, the link to the URL where we can find the generated QR code. You can read about all of these attributes here , but for our purposes, all we need is the actual message content. 

 

# using dictionary comprehension to return readable responses
readable_responses = {campaign: response.json() for campaign, response in responses.items()}

Each key in readable_responses is a campaign name, and each value is a list of dictionaries containing information about Flowcodes associated with that campaign.




To use these Flowcodes in our software, we need their links, which we can extract from readable_responses.

"""
Creates a dicionary called generated_urls, with campaigns as keys and a list of ids and qr code urls as values.
"""
generated_urls = {}
for campaign, urls in readable_responses.items():   

    generated_urls[f'{campaign}'] = [] 

    for url in urls: 

        generated_urls[f'{campaign}'].append(

            {"id": url['id'], 

             "qr_code": url['images'][0]['url']

            })

 

This will create our final object, which captures information about all of our campaigns, the advertisements associated with them, and the urls allowing us to access all of the QR codes that we just generated.

 

 

GET STARTED WITH THE FLOWCODE API

Generating Responsive Flowcodes for Each Campaign Using The API

Earlier, we mentioned that you can add rules that govern the behavior of your Flowcodes. Here, we’ll show you how to do so.

Flowcodes allow you to add conditional rules to them when creating them with a smart_rules argument. With smart rules, you can configure the response URL based on device type, day of the week, and time of day. They can be injected into Flowcodes programmatically when making the API call using a smart_rules() function:

def smart_rule_generator(id):
    smart_rules = {
        "rules": [
            {"url": f"https://www.flowcode.com?type=1&id={id}",    
            "device_type": "ANDROID" },
            {"url": f"https://www.flowcode.com?type=1&id={id}",
            "device_type": "IOS" },
            {"url": f"https://www.flowcode.com?type=2&id={id}",
            "days": [1, 2, 3, 4, 5],
            "device_type": "DESKTOP" },
            {"url": f"https://www.flowcode.com?type=3&id={id}",
            "days": [0, 6],
            "device_type": "DESKTOP" } ], # "time_zone": "SCANNER" means that time of day and day of the week is # that of the person scaning the QR Code
            "time_zone": "SCANNER" }
    return smart_rules

campaign_data = []
for campaign in campaigns:  
    campaign_data.append(ad_data.loc[ad_data["xyz_campaign_id"] == campaign])

for campaign in campaign_data:
    campaign_list = []
    for index, row in campaign.iterrows():
        campaign_list.append(
            {"id": f"{row.ad_id}",
            "url_type": "URL",
            "url": f"http://www.flowcode.com?id={row.ad_id}"
            "smart_rules": smart_rule_generator(row.ad_id)})  
    flowcodes.append(campaign_list)

 

These smart rules add a type=1 argument to the URL if the user’s on Android or IOS, a type=2 argument if the user is on a desktop and is searching on any weekday, and a type=3 argument if the user is on a desktop and searching on the weekend.

For our ad database, you could imagine using this to customize a store page linked to the marketing materials to show different information to mobile users.

Downloading Flowcodes from URLs

The Flowcodes that we generated come in the form of SVG files (scalable vector graphics), which are similar to JPGs and PNGs, but preserve their resolution when rescaled.

We can use Python to download all of these images and save them to a local folder. 

import os # used to make new directories
parent_dir = "<parent_dir>" # create a new directory to store images
root_dir = parent_dir + '/flowcode_images'
if not os.path.exists(root_dir):
    os.mkdir(root_dir)
for campaign, urls in generated_urls.items(): 

    campaign_dir = root_dir + f'/{campaign}' 

    os.mkdir(campaign_dir) 

    for url_object in urls: 

        id = url_object['id'

        r = requests.get(url_object['qr_code'], allow_redirects=True

        file_url = "".join([campaign_dir, f"/{id}.svg"]) 

        with open(file_url, 'wb') as handler: 

            handler.write(r.content) 

print(f"Flowcodes created at {root_dir}")

This code takes in a directory path parent_dir, and creates a flowcode_images folder with subfolders for each campaign. Within each campaign folder, all Flowcodes are SVGs, with a name given by their id.

If you don’t know what a directory path is or how to find them, check out this helpful guide .

Conclusion

And with that, we’re done! We converted a dataset of ads into Flowcodes, created campaigns to organise them, attached smart rules to them, and converted them into a flexible file format that can be inserted almost anywhere.

Once you’ve created these Flowcodes, you can easily view and export the analytics for them from our website, allowing you to make much more informed decisions about your offline marketing.

This is just one use case for our API. With very similar steps, you can create unique identifiers for your products to track inventory, or create an interface for presenting information to your customers, or link your offline marketing directly to your online store.

A full outline of our Flowcode API is available here, and documentation for the Flowpage API and Analytics API will be available soon! Please reach out to [email protected] if you need any assistance or have any questions about the API.

Simplifying The Process

This code involves a few different steps, and so to simplify it, we wrote a function that takes in all of the arguments we discussed above and generates Flowcode SVGs. Feel free to adapt it when creating your own Flowcodes with the API! 

 

import pandas as pd 

import requests 

import re 

import os 



def generate_flowcodes(client_id, id_column_name, campaign_column_name,   

                       redirect_url, dataset, smart_rules={}, 

                       parent_dir="", pass_id_as_argument=True): 

    """

    Generates a set of flowcodes from a database, saving them as local svg    

    files. 

    

    Parameters 

    ---------- 

    client_id: str 

        unique identifier used to authenticate users for the API 

    id_column_name: str 

        the name of the column containing object ids in the dataset 

    campaign_column_name: str 

        the name of the column containing campaign ids in the dataset 

    redirect_url: str 

        the url that the flowcodes will redirect to 

    dataset: pd.Dataframe 

        dataset of object and campaign ids; must be a pandas DataFrame 

    smart_rules: dict 

        a smart rules object, of the form specified in the documentation; 

        if left blank, will be ignored 

    parent_dir: str 

        the path to the directory where the svg files will be saved; if   

        blank, will use the current working directory 

    pass_id_as_argument: bool 

        whether to create unique links to redirect_url for each flowcode   

        using their ids as url arguments 


    Outputs 

    ------- 

    None 


    Side Effects 

    ------------ 

    Saves a folder containing all generated flowcodes into the specified  

    directory, creating all associated urls and campaigns 

    """ 


    # STEP ZERO: ERROR CHECKING
    _error_checking(client_id, dataset, id_column_name,  
                    campaign_column_name)

    # setting parent directory
    if not parent_dir:
        parent_dir = os.getcwd()

    dataset = dataset.loc[:, [id_column_name, campaign_column_name]]
    campaigns = dataset[campaign_column_name].unique()

    # STEP ONE: GENERATING CAMPAIGNS
    _generate_campaigns(campaigns=campaigns, client_id=client_id,
                        id_column_name=id_column_name,
                        campaign_column_name=campaign_column_name,
                        dataset=dataset,
                        reserved_urls=False)

    # STEP TWO: PRE-PROCESSING URLS
    flowcodes = _preprocess_urls(campaigns=campaigns,
                                id_column_name=id_column_name,
                                campaign_column_name=campaign_column_name,
                                redirect_url=redirect_url,
                                dataset=dataset,
                                pass_id_as_argument=pass_id_as_argument)


    # STEP THREE: SENDING URL POST REQUESTS FOR EACH CAMPAIGN
    responses = _generate_urls(flowcodes=flowcodes,
                              campaigns=campaigns,
                              client_id=client_id,
                              smart_rules=smart_rules)

    # STEP FOUR: ACCESSING QR CODE REQUESTS
    generated_urls = _process_url_responses(responses)

    # STEP FIVE: CREATING SVGS
    _generate_svgs(parent_dir=parent_dir,
                  generated_urls=generated_urls)

    return "Flowcodes Generated"  


def _error_checking(client_id, dataset, id_column_name, campaign_column_name):
    # checking client_id
    regex = "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}"

    if not isinstance(client_id, str):
        raise Exception(f"The client id must be a string, but is a:  
                        {type(client_id)}")

    if not re.fullmatch(regex, client_id):
        raise Exception("Your client id doesn't look like it's the right 
                        format" "(aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa)")


    # checking if we're using a pandas dataframe
    if not isinstance(dataset, pd.DataFrame):
        raise Exception("The dataset argument must be a Pandas Dataframe")

    if id_column_name not in dataset.columns:
        raise Exception("The id column is not specified correctly. \n"
                        "Make sure it's a name, not an index!")

    # checking if the columns exist in the dataframe
    if campaign_column_name not in dataset.columns:
        raise Exception("The campaigns column is not specified correctly."
                        "Make sure it's a name, not an index!")


def _generate_campaigns(campaigns, client_id, id_column_name, 

                        campaign_column_name, dataset, reserved_urls): 

    campaign_url = "https://api.flowcode.com/v2/flowcode/batch/bulk-campaign" 

    print("Creating Campaigns!"

    for campaign in campaigns: 

        data = {"name": f"{campaign}"

                "display_name": f"{campaign} display"

                "client_id": client_id, 

                "reserved_urls_unique": reserved_urls } 

        try

            response = requests.post(campaign_url, data)  

            response.raise_for_status() print(response.text) 

        except requests.exceptions.HTTPError as err: 

            if err.response.status_code == 409

                print(f"Campaign {campaign} already exists! Skipping 

                        creation"

            else

                raise err 

    return campaigns


def _preprocess_urls(campaigns, id_column_name, campaign_column_name,
                    redirect_url, dataset, pass_id_as_argument):
    campaign_data = []
    for campaign in campaigns:
        #get all rows where the campaign id is equal to our active campaign
        campaign_data.append(dataset.loc[dataset[campaign_column_name] ==  
        campaign])
    flowcodes = [] # initialise an empty list to store our url data
    for campaign in campaign_data:
        campaign_list = []
            for index, row in campaign.iterrows():
                if pass_id_as_argument:
                    id_string = f"{id_column_name}"
                    url_string = "".join([redirect_url,   
                                          f"/id={row[id_string]}"])
 
                else:
                    url_string = redirect_url
                campaign_list.append({"id": f"{row[id_string]}",  
                                      "url_type": "URL",
                                      "url": url_string})
        flowcodes.append(campaign_list)
    return flowcodes


def _generate_urls(flowcodes, campaigns, client_id, smart_rules):
    codes_url = "https://api.flowcode.com/v2/flowcode/batch/bulk"
    responses = {}

    # send a request for each campaign in "flowcodes"
    for index, value in enumerate(flowcodes):
        if not value: # deals with errors caused by empty campaigns
            pass
        else:
            if smart_rules:
                codes_data = {"client_id": client_id,
                              "campaign_name": f"{campaigns[index]}",
                              "urls": flowcodes[index],
                              "smart_rules": smart_rules }
            else:
                codes_data = {"client_id": client_id,
                              "campaign_name": f"{campaigns[index]}",
                              "urls": flowcodes[index]}

        # send the data as a json to support the formatting of "urls"
        try:
            flowcode_response = requests.post(codes_url, json=codes_data)
            flowcode_response.raise_for_status()
            responses[f'{campaigns[index]}'] = flowcode_response
        except requests.exceptions.HTTPError as err:
            if err.response.status_code == 409:
                print(f"Some URLS in Campaign {campaigns[index]} already
                        exist!" "Skipping creation")
            else:
                raise err
    return responses

def _process_url_responses(responses):
    print(responses)
    readable_responses = {campaign: response.json() for campaign, response   
                          in responses.items()}
    generated_urls = {}
    for campaign, urls in readable_responses.items():
        generated_urls[f'{campaign}'] = []
        for url in urls:
            generated_urls[f'{campaign}'].append({"id": url['id'],
                            "qr_code": url['images'][0]['url']})
    return generated_urls


def _generate_svgs(parent_dir, generated_urls):
    # create a new directory to store images
    root_dir = parent_dir + '/flowcode_images'
    if not os.path.exists(root_dir):
        os.mkdir(root_dir)
    for campaign, urls in generated_urls.items():
        campaign_dir = root_dir + f'/{campaign}'
        os.mkdir(campaign_dir)
        for url_object in urls:
            id = url_object['id']
            r = requests.get(url_object['qr_code'], allow_redirects=True)
            file_url = "".join([campaign_dir, f"/{id}.svg"])
            with open(file_url, 'wb') as handler:
                handler.write(r.content)

GET STARTED WITH THE FLOWCODE API

« Back to Blog