How to Build a Social Media Widget with the Taggbox API and AI (No Coding Needed)

People are already posting about your brand on Instagram, LinkedIn, YouTube, X, TikTok and Google. A social media feed widget puts all of it in one place, live, in your design. This guide gives you a prompt that makes an AI tool build the page for you. Pick the tool you already have open, paste, done.

What is a social widget?

A live page that shows posts from many networks together. You see them on event screens, on homepages, in stores, offices and stadiums.

Taggbox does the hard part. It connects to every network, collects the posts, lets you approve or hide them, and keeps them fresh. This guide covers the last step: showing those posts in a page that is completely yours.

Why use the Taggbox API instead of the embed code?

The Taggbox embed code puts a themed widget on your site in two minutes. Use it if that is all you need.

Use the Taggbox API when you want more:

  • Your own design. Your layout, your fonts, your brand. No theme limits.
  • Any screen. A 4K event display, a kiosk, a page inside your own app.
  • Anywhere on the website, or page inside your app, or even on your event screens as a social wall or venue signage.
  • One connection instead of five. You never deal with Instagram or LinkedIn directly, their logins or their rule changes. Taggbox hands you approved posts in one clean format.

And you no longer need a developer to use it. An AI tool writes the page; you paste one prompt.

How do you get your access token?

Every request needs a token so Taggbox knows which widget to show.

  1. Log in to your Taggbox dashboard.
  2. Open the gallery you want the posts from, or create one.
  3. On that gallery’s card, click the ⋮ (three dots) menu.
  4. Click Access Token and copy the value.

Which token should you use? A gallery token starts with wt1_ and opens that one gallery and nothing else, so it is safe to give to an agency or paste into a single website. Your account key opens every gallery you own. Use the gallery token unless you have a reason not to.

Treat it like a password. Anyone with it can read that gallery’s approved posts. The prompt below tells the AI to keep it on the server and never in the page.

API access is included in {{PLAN_NAME}}.

Which request does your widget need?

Just one:

GET https://api.taggbox.com/api/v3/posts?limit=24
Authorization: Bearer wt1_YOUR_ACCESS_TOKEN

You do not have to run this yourself. It is here so you can see how small the moving part really is — and so you can check it with curl if the page ever comes back empty.

What an AI gets wrong unless you tell it

AI tools have read a lot of other social-wall APIs, and that muscle memory produces code that looks right and returns nothing. These are the five that matter. They are all in the prompt below, which is why the prompt is worth pasting whole.

IT WILL ASSUMEACTUALLY
Posts are at the top level of the JSONThey are at body.posts — every response is enveloped
?fields= slims the responseThere is no fields parameter; the full post object always comes back
media[0] is the imagemedia[0] can be a video file. Take the first entry whose type is “image”, then its cdn_url
Missing values are “” or 0They are null — including author.name, which falls back to author.handle
The default sort needs fixingIt is already right: pinned first, then newest

Write it yourself: PHP

If you would rather not involve an AI at all, here is the whole thing. Save it as index.php, set the two environment variables, and run php -S localhost:8080. Nothing to install.

<?php
// index.php - one file: fetch, cache, render. Run: php -S localhost:8080
const CACHE_TTL = 300; // 5 minutes
$base = rtrim(getenv('API_BASE_URL') ?: 'https://api.taggbox.com/api', '/');
$token = getenv('ACCESS_TOKEN') ?: '';
$dir = __DIR__ . '/cache';
$file = $dir . '/posts.json';
 
function fetchPosts(string $base, string $token): ?array {
    $ch = curl_init("$base/v3/posts?" . http_build_query(['limit' => 24]));
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
    ]);
    $raw = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($raw === false || $code !== 200) return null;
    $json = json_decode($raw, true);
    // the payload sits inside the envelope: { status, code, body }
    return ($json['status'] ?? false) ? ($json['body']['posts'] ?? []) : null;
}
 
function getPosts(string $base, string $token, string $dir, string $file): array {
    if (is_file($file) && time() - filemtime($file) < CACHE_TTL) {
        return json_decode(file_get_contents($file), true) ?? [];
    }
    $posts = fetchPosts($base, $token);
    if ($posts !== null) {
        if (!is_dir($dir)) @mkdir($dir, 0775, true); // first run
        $tmp = $file . '.' . getmypid() . '.tmp'; // write then rename, so
        if (@file_put_contents($tmp, json_encode($posts)) !== false) {
            @rename($tmp, $file); // nobody reads half a file
        }
        return $posts;
    }
    // refresh failed: the last good copy beats an empty page, at any age
    return is_file($file) ? (json_decode(file_get_contents($file), true) ?? []) : [];
}
 
$posts = getPosts($base, $token, $dir, $file);
$e = fn($v) => htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
 
function firstImage(array $p): ?string { // media[0] can be a video FILE
    foreach ($p['media'] ?? [] as $m)
        if (($m['type'] ?? '') === 'image') return $m['cdn_url'];
    return null;
}
 
function permalink(array $p): ?string { // escaping alone is not enough
    $u = $p['source']['permalink'] ?? '';
    return preg_match('#^https?://#i', $u) ? $u : null;
}
?>
<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<title>What people are saying</title></head>
<body>
<h1>What people are saying</h1>
<?php if (!$posts): ?><p>No posts to show yet.</p><?php endif; ?>
<?php foreach ($posts as $p): $img = firstImage($p); $url = permalink($p); ?>
<article>
<?php if ($img): ?>
<img src="<?= $e($img) ?>" alt="" loading="lazy" width="300">
<?php endif; ?>
<p><strong><?= $e($p['author']['name'] ?? $p['author']['handle'] ?? 'Unknown') ?></strong>
<small><?= $e($p['network']['name'] ?? '') ?></small></p>
<p><?= nl2br($e($p['content']['text'] ?? '')) ?></p>
<?php if ($url): ?>
<a href="<?= $e($url) ?>" rel="noopener noreferrer">View post</a>
<?php endif; ?>
</article>
<?php endforeach; ?>
</body></html>

Write it yourself: Node.js

The same page in Node.js 18 or newer, which has fetch built in. Save it as server.js, run npm install express, then node server.js.

// server.js - npm install express, then: node server.js
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
const BASE = (process.env.API_BASE_URL || 'https://api.taggbox.com/api')
    .replace(/\/$/, '');
const TOKEN = process.env.ACCESS_TOKEN || '';
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
const CACHE_DIR = path.join(__dirname, 'cache');
const CACHE_FILE = path.join(CACHE_DIR, 'posts.json');
 
async function fetchPosts() {
    const res = await fetch(`${BASE}/v3/posts?limit=24`, {
        headers: { Authorization: `Bearer ${TOKEN}` },
    });
    const json = await res.json().catch(() => null);
    // a request can fail while still returning HTTP 200
    if (!res.ok || !json || json.status !== true) {
        throw new Error(json?.message || `API error ${res.status}`);
    }
    return json.body.posts || []; // payload is inside body
}
 
function readCache() {
    try { return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); }
    catch { return null; }
}
 
async function getPosts() {
    try {
        const age = Date.now() - fs.statSync(CACHE_FILE).mtimeMs;
        if (age < CACHE_TTL) return readCache() || [];
    } catch { /* no cache yet - fall through and fetch */ }
    try {
        const posts = await fetchPosts();
        fs.mkdirSync(CACHE_DIR, { recursive: true }); // first run
        const tmp = `${CACHE_FILE}.${process.pid}.tmp`; // write then rename, so
        fs.writeFileSync(tmp, JSON.stringify(posts)); // nobody reads half a file
        fs.renameSync(tmp, CACHE_FILE);
        return posts;
    } catch (err) {
        console.error(err.message);
        return readCache() || []; // last good copy, any age
    }
}
 
const esc = (s = '') => String(s).replace(/[&<>"']/g,
    c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c]));
const safe = u => /^https?:\/\//i.test(u || '') ? u : null;
 
function card(p) {
    const img = p.media?.find(m => m.type === 'image')?.cdn_url; // not media[0]
    const url = safe(p.source?.permalink);
    return `<article>
${img ? `<img src="${esc(img)}" alt="" loading="lazy" width="300">` : ''}
<p><strong>${esc(p.author?.name || p.author?.handle || 'Unknown')}</strong>
<small>${esc(p.network?.name || '')}</small></p>
<p>${esc(p.content?.text || '')}</p>
${url ? `<a href="${esc(url)}" rel="noopener noreferrer">View post</a>` : ''}
</article>`;
}
 
app.get('/', async (_req, res) => {
    const posts = await getPosts();
    res.send(`<!doctype html><html lang="en"><head><meta charset="utf-8">
<title>What people are saying</title></head><body>
<h1>What people are saying</h1>
${posts.length ? posts.map(card).join('') : '<p>No posts to show yet.</p>'}
</body></html>`);
});
 
app.listen(PORT, () => console.log(`http://localhost:${PORT}`));

Both versions do the same five things: call the API once, cache the answer for five minutes, fall back to the last good copy when the API is unreachable, escape everything they print, and keep the token on the server.

The prompt

Paste this into whichever AI tool you use — Claude Code, Cursor, Codex, Copilot, ChatGPT or Gemini. It is self-contained: everything the AI cannot guess is written into it, so it still works if the tool cannot open links.

Build me a social widget: a web page, rendered by my own server, that
shows a live feed of the social posts collected in my Taggbox gallery.
 
The API is documented here - read it before writing anything, because
the field names below are not the ones other social-wall APIs use:
https://raw.githubusercontent.com/wallapi/taggbox.com-API-Docs/main/llms.txt
 
What to deliver - both of these, in this reply, not a choice:
- index.php: ONE self-contained PHP 8 file with everything in it, the
  API call, the cache, the HTML and the CSS.
- server.js + package.json: the same four things again in Node.js 18+
  with Express. No separate stylesheet in either version.
- README.md covering both: the files, the two environment variables,
  how to run each one written for someone who has never opened a
  terminal, how the cache works, and what to check when it goes wrong.
 
Calling the API
- GET https://api.taggbox.com/api/v3/posts?limit=24, with the header
  Authorization: Bearer <my token>.
- Take the token from the ACCESS_TOKEN environment variable and the
  base URL from API_BASE_URL. Neither belongs in the source.
- The response is wrapped. Posts are at body.posts and paging at
  body.paging - never at the top level. A request can fail while
  returning HTTP 200, so check the envelope's `status` flag too.
- Do not add a `fields` parameter; it does not exist.
- Do not touch `sort`. Pinned first, newest after, is already default.
 
Caching - this is code you write, not a file you hand me
- Keep the last response in a local JSON file and reuse it until it is
  5 minutes old. Put that 5 in one named constant at the top.
- Create the cache directory yourself on first run; a fresh copy with
  no cache must work, not crash.
- Write to a temp file and rename it into place, so a visitor arriving
  mid-write never gets half a JSON document.
- When a refresh fails, keep serving the cached copy however old it is.
  Render the empty state only if nothing has ever succeeded.
 
What each post gives you
- author.name, or author.handle when the name is null - either can be
  null, so handle that rather than printing "null".
- network.name, content.text (plain text), created_at for the date.
- The image is the FIRST entry in `media` whose type is "image", read
  from its cdn_url. Do not reach for media[0]: that is often a video.
- source.permalink links back; add rel="noopener noreferrer".
 
Non-negotiable
- Every API call happens on the server. The token must not reach the
  browser - not in the HTML, not in a comment, not in an attribute.
- Escape everything you print; allow only http and https URLs.
 
Finish by commenting each part, then ask me for my access token and
tell me how to set the environment variables and run each version.

Set up your AI agent once

Every agent reads a project context file before it writes anything. Drop one in, and every later request can be a single line — “add a network filter”, “switch the cache to Redis” — because the rules are already in the project.

The contents are the same for every tool. Only the filename and location change, so write this once and save it at the path your tool expects:

# Taggbox social widget - project context
 
Data source: GET https://api.taggbox.com/api/v3/posts
Full spec: https://raw.githubusercontent.com/wallapi/taggbox.com-API-Docs/main/llms.txt
 
Rules for all code in this project:
- Read the token from ACCESS_TOKEN and the base URL from API_BASE_URL.
  Never hard-code either.
- Render on the server. The token must never reach the browser.
- The payload is inside the envelope: body.posts / body.paging. Check the
  HTTP status AND the envelope's own `status` flag.
- There is no `fields` parameter, and the default sort (pinned first,
  then newest) is already correct - do not change it.
- The image is the first `media` entry whose type is "image", via its
  cdn_url. media[0] is often a video file.
- Absent values are null, never "" or 0. author.name falls back to
  author.handle.
- Cache to a JSON file for 5 minutes: create the directory on first run,
  write through a temp file and rename it into place, and serve the last
  good copy if a refresh fails.
- Escape everything printed; allow only http(s) URLs in href and src.
- Build both an index.php and a server.js, each self-contained with its
  CSS inside it, plus one README.md covering both.
TOOLSAVE IT AS
Claude CodeCLAUDE.md in the project root
OpenAI Codex / GrokBuild AGENTS.md in the project root
Google Antigravity / Gemini CLIGEMINI.md in the project root
Cursor.cursor/rules/taggbox.mdc — add alwaysApply: true to its frontmatter
GitHub Copilot.github/copilot-instructions.md
Windsurf.windsurf/rules/taggbox.md
Cline.clinerules
AiderCONVENTIONS.md, passed with aider –read CONVENTIONS.md
ChatGPT, Gemini, Claude.ai (browser)no file — paste it as your first message, or into Custom Instructions

With that file in place, the long prompt above becomes optional: “Build the Taggbox widget described in the context file, both languages” is enough.

What you get back

Two complete versions, so you can hand whichever one fits your hosting to whoever deploys it. Each is self-contained — the CSS is inside the file, so there is no stylesheet to wire up.

PHPNODE.JS
index.php — one file: the API call, the cache, the HTML and the CSS. Nothing to install.server.js with the same four things, plus package.json.

Plus a README.md that documents both: where each file goes, how to set the two environment variables, how to run each version, and what to check when something looks wrong.

Neither version ships a cache file. Each creates its own cache directory on first run, writes through a temporary file so a visitor never catches a half-written one, and keeps serving the last good copy if the API is unreachable.

Setting the two values

One you fetch, one you already know:

VALUEENVIRONMENT VARIABLEWHERE IT COMES FROM
TokenACCESS_TOKENyour dashboard — the four steps above
API base URLAPI_BASE_URLalways https://api.taggbox.com/api

The AI asks you for the token at the end of its reply, after the code is written, so you are never waiting on a question before you have anything. Put both in a .env file (or your host’s environment settings) — never in the code itself, and never in a chat window.

If it goes wrong

Blank page, no error. The gallery has no approved posts yet, or the filters in your dashboard are hiding all of them. Check the gallery first, not the code.

401 from the API. The token is missing or wrong in the environment, or API access is not enabled on the account.

Posts look stale. That is the five-minute cache doing its job. Delete the cache file to force a refresh.

The AI asked questions instead of writing code. Reply: “Build it now with sensible defaults and ask me for the token at the end.”

How many API calls is this?

About 288 a day, whatever your traffic, because the page serves a cached copy for five minutes at a time. Two things break that number: running several PHP workers or Node instances that each keep their own cache file, and paging through posts without caching each page. The prompt covers both.