Pulling Your Behance Portfolio Into Your Own Site: Pagination, Rate Limits, and a 24-Hour Cache
Pulling your Behance portfolio into your own site is easy to start and easy to get wrong. Here's the pagination loop, the caching strategy that keeps you under the rate limit, and how to verify the API still behaves the way you expect.
28 Feb 2026, 21:22 UTC

The problem: your portfolio lives on Behance, your audience lands on your site
If you publish work on Behance but also run a personal site, you face a small but annoying duplication problem: every new project means updating two places. The obvious fix is to have your site pull your published projects straight from Behance's API. The less obvious part is doing it without hammering the API on every page view, blowing through the hourly request allowance, or shipping a page that renders one skeleton loader per project.
The approach that works well in practice: fetch all pages of the project list server-side, cache the merged result for a day, and revalidate with a conditional request. Here's the shape of that solution, plus the constraints that will bite you if you ignore them.
Note: Behance's API terms, endpoint availability, and rate limits have changed over time. Treat the specifics below as a design to verify against current documentation and live responses before you build on them — verification steps are included at the end.
What the projects endpoint gives you
The documented pattern for listing a creator's published work is a GET request to /v2/users/{user_id}/projects, authenticated with your API token. Each project object includes fields such as id, name, url, covers (thumbnail images at several sizes), and basic stats like view and appreciation counts. That's enough to render a portfolio grid with cover images linking back to Behance.
Two things are not in there, and both matter:
- Drafts and private projects are excluded. Only published work appears, so you can't accidentally leak work-in-progress — but you also can't use the API to preview unpublished projects on your site.
- Full project content isn't included. The list endpoint gives you metadata and covers, not the individual images and text modules inside a project. If you want deep project pages on your own site, you need additional requests per project, which changes the caching math considerably.
Pagination: read pages, don't guess
The endpoint paginates with page and per_page query parameters, with a per-page cap commonly documented as 20. Each response carries total, page, pages, and per_page. The robust pattern is: request page 1, read pages from the response, then fetch the remaining pages and concatenate. Don't hard-code an expected page count — a creator who publishes a new project can shift the total at any time.
A minimal Node.js fetch loop (run server-side, never in the browser, since your token must stay secret):
const TOKEN = process.env.BEHANCE_TOKEN;\nconst USER = \"your-user-id\";\n\nasync function fetchAllProjects() {\n const get = (page) =\u003e\n fetch(`https://www.behance.net/v2/users/${USER}/projects?page=${page}\u0026per_page=20`, {\n headers: { Authorization: `Bearer ${TOKEN}` },\n }).then((r) =\u003e {\n if (!r.ok) throw new Error(`Behance API ${r.status}`);\n return r.json();\n });\n\n const first = await get(1);\n const rest = [];\n for (let p = 2; p \u003c= first.pages; p++) rest.push(await get(p));\n return [first, ...rest].flatMap((d) =\u003e d.projects);\n}A sanity check worth keeping: after merging, compare projects.length against the total field from page 1. If they disagree, the list changed mid-fetch or your pagination logic is off — log it rather than silently serving a partial portfolio.
The rate limit is the real design constraint
The documented allowance is on the order of 150 requests per hour per token, with X-RateLimit-Limit and X-RateLimit-Remaining response headers telling you where you stand. That sounds generous until you do the math on a naive implementation: a 60-project portfolio costs 3 requests per visitor page view if you fetch live. Fifty visitors an hour and you're done — everyone else gets 429s.
The fix is boring and effective: cache the merged project list server-side for 24 hours. Portfolio content changes rarely; a day of staleness is a fine trade for staying two orders of magnitude under the limit. Where your platform supports it, use ETag-based conditional requests on revalidation — if nothing changed, you get a cheap 304 instead of a full payload.
Two cautions on the limit itself:
- Don't hard-code 150. Read the
X-RateLimit-*headers at runtime and back off whenRemaininggets low. Limits can change per token or over time, and a hard-coded assumption is how you end up with a production 429 storm. - Cache failures too, briefly. If the API errors, serve the stale cache and retry after a short cooldown rather than on every request. A portfolio section that degrades to yesterday's data beats one that degrades to an empty grid.
Trade-offs you're accepting
This design optimizes for a read-heavy, slowly-changing portfolio. The costs: new projects take up to a cache lifetime to appear (mitigate with a manual cache-bust button if your CMS allows it); stats like appreciation counts go stale, so either hide them or label them as approximate; and you're dependent on a third party's uptime and API policy for a core page of your site. If Behance's API access terms don't fit your situation, the fallback is exporting your project data periodically and treating your own storage as the source of truth — more manual, but fully under your control.
Verify before you ship
Because endpoint details and limits drift, confirm the live behavior from a terminal before writing production code (replace the placeholders; the token needs API access granted by Behance):
curl -i -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"https://www.behance.net/v2/users/YOUR_USER_ID/projects?page=1\u0026per_page=20\"Check three things in the response: the JSON body contains a projects array plus total/page/pages fields; the headers include X-RateLimit-Limit and X-RateLimit-Remaining; and the HTTP status is 200. Then run your pagination loop once and confirm the merged count equals total. If any of that doesn't match, the API has moved — update the design, not just the code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.