Automating Deal Ads with n8n, Bannerbear, and Meta Graph API

At Zomunk, we monitor flight deals. Whenever we find a deal that is really good, usually 40-90% off, we share it with our subscribers.

The real challenge with running ads on these deals is the unpredictable lifespan of a deal. Some stay active for days, while others disappeared in a few hours.

Manually managing this became a massive operational bottleneck. Every single deal required a designer to create a creative, a marketer to take it live, and someone to manually monitor the expiry to pause the ad. Not very efficient, you see?

We solved this by building an automation pipeline. Now, whenever our backend sends a new deal via a webhook, the system autonomously designs the ad, writes the copy, and publishes it to Meta in seconds.

Here’s the technical breakdown of how we built this using n8n, Bannerbear, and the Meta Graph API.

The trigger

Our backend sends a JSON payload containing the deal properties such as origin, destination, price, and deep links.

If the JSON contains a create flag, we start the ad creation flow.

This is obviously business-specific and will not be the same in your case.

The Logic Switch (If/Else Node): We route the workflow based on deal property:

  • if it has create → Start Ad Creation Flow.
  • if it has update → Check Expiry Logic (The “Kill Switch”).

Automated Design with Bannerbear

We use a Bannerbear template with placeholders for the City Name, Price, and Background Image. We send a POST request to Bannerbear with the specific text and image URLs mapped from the webhook.

Depending on your template design, it should look something like this

JSON
{
  "template": "Rqg32K5QN---AY",
  "modifications": [
    { "name": "bg_img", "image_url": "<your-image-path>" },
    { "name": "city_name", "text": "Amsterdam" },
...
  ]
}

Bannerbear is fast, but image rendering isn’t instant. Hence we need a WAIT node (I prefer waiting for 5s).

After the wait, we perform a GET request using the uid from the Bannerbear response to get the final .png URL, and then use an n8n HTTP Request node to download the file as binary data.

Meta Graph API Setup (Tokens & Permissions)

Before interacting with Meta API, you need a valid Access Token.

Uploading the Image to Meta

Meta doesn’t let you just paste an image URL into an ad. You must upload the binary file to their servers first and get an Image Hash.

  • Endpoint: POST https://graph.facebook.com/v20.0/act_{AD_ACCOUNT_ID}/adimages
  • Body Content Type: multipart/form-data
  • Input: The binary data from the previous Bannerbear download step.

Response: You will receive a hash (e.g., 509bb2b8fe...). Save this. You need it to build the creative.

Creating the “Dark Post” (Ad Creative)

This is where I spent the most amount of time debugging. tl;dr: If you use asset_feed_spec, Meta assumes you want a “Dynamic Creative,” which limits you to one ad per Ad Set. To create standard ads, you must use object_story_spec with link_data. Also make sure you are using instagram_user_id and not instagram_actor_id which is deprecated.

Your JSON payload will look something like this

JSON
{
  "name": "id or some unique name",
  "object_story_spec": {
    "page_id": "<id>",
    "instagram_user_id": "<id>",
    "link_data": {
      "link": "landing page link",
      "message": "Primary text goes here",
      "name": "headline",
      "image_hash": "{{ Object.values($json.images)[0].hash }}",
      "call_to_action": { "type": "BOOK_NOW" }
    }
  }
}

Note the use of Object.values to handle dynamic filenames.

Taking the Ad Live

Finally, we link the Creative to an Ad Set.

Endpoint: POST https://graph.facebook.com/v20.0/act_{AD_ACCOUNT_ID}/ads

JSON
{
  "name": "ad-name",
  "adset_id": "adset-id",
  "creative": { 
    "creative_id": "{{ $json.id }}" 
  },
  "status": "ACTIVE",
  "instagram_user_id": "instagram-user-id",
  "tracking_specs": [
    { 
      "action.type": ["app_custom_event"],
      "application": ["id from the dropdown you see while setting up app event tracking inside an ad"]
    }
  ]
}

The “Kill Switch” (Handling Updates)

This may not be relevant to most businesses but we pause an ad (or OOS in catalog) when the deal is no longer active.

We use the Batch API to update the catalog instantly.

Endpoint: POST https://graph.facebook.com/v22.0/{CATALOG_ID}/batch

JSON
{
  "requests": [
    {
      "method": "UPDATE",
      "retailer_id": "{{ $('Webhook').item.json.body.data.payload.id }}",
      "data": { "availability": "out of stock" }
    }
  ]
}

Pause the Ad

Since we don’t store the Meta Ad ID in our database, we find it by searching for the Deal ID (which we used as the Ad Name in Step 6).

  • Search: GET /ads?filtering=[{'field':'name','operator':'CONTAIN','value':'{{DEAL_ID}}'}]
  • Action: POST /{AD_ID} with body {"status": "PAUSED"}

This automation turned a manual, 30-minute process into a 15-second autonomous flow. Our marketing bandwidth in managing ads for deals is effectively zero. By coupling n8n’s logic with Meta’s Graph API, we also ensure that Zomunk never advertises a deal that doesn’t exist.

In

Leave a Reply

Your email address will not be published. Required fields are marked *