Facebook CAPI + BQ. A ROAS Architects fork of Stape's server-side tag, Apache 2.0.

The same Facebook CAPI tag, with the send path on the record.

Every request it makes and every response it gets back, written to a BigQuery table you own. Everything else is upstream's.

Read the template on GitHub
01 The Same Tag

Upstream's tag, plus a log table.

This is Stape's server-side Facebook Conversions API template. It builds Conversions API events from the requests already passing through your server container and sends them to the Graph API, exactly as it does upstream. We did not rewrite it and we did not try to improve it.

One thing is different. Every request the tag sends, and every response it gets back, is written to a BigQuery table you own. What goes to Facebook is untouched.

02 What Upstream Took Out

Stape removed the logging, and that was a fair call.

Between 2026-05-26 and 2026-07-01 they took the logging out of their server-side tag fleet. For this template it went in one commit, which removed the BigQuery path and the console path together. The template ships today with no logging at all.

That is defensible. A template that has to run in anybody's container is carrying a path most installs never switch on, and carrying it costs code, permissions and support. Their work is good, and a fork like this one is only possible because of the licence they chose.

We need it back, for a reason specific to how we work rather than a criticism of that decision. A server-side tag fails quietly. There is no browser console to check and no user to complain. It keeps returning success to the container while the platform on the other end rejects everything it sends, and that can run for a day before anybody notices, because noticing depends on a person deciding to go and look. When it happened to us, the tag we could not diagnose was the one with no log table behind it. Its siblings, still logging, we could simply query.

So this fork restores the logging by reverting that one commit. The revert applies with no conflicts, because upstream has not changed the template since.

03 What You Get To Query

Every request, and what came back, in a table you own.

Logging stays off until you switch it on and name a project, dataset and table. After that the tag writes as it works. The table's columns, what lands in each one, and the edge cases that surprise people are documented in the repository, which is where they stay correct.

11 columnsThe request, the response, the event name and the status code, in a table in your own project. The schema and its DDL live in the repository, not here.
3 log pointsA row before each call to Facebook, a row when it comes back whatever the status, and a row if the batch fails as a whole rather than call by call.
1 revertThe logging is upstream's removal commit, put back. The rest of the diff is the three fixes below, the documentation Stape never wrote, and the branding the gallery requires. git diff upstream-mirror..main is the whole of it, and is the only fork documentation there is.
0 changesNothing about what the tag sends to Facebook is different. The logging is a side channel off the send path, not a change to it.

The row that earns the fork is the response row. A 400 from Facebook lands with its status code and Facebook's own error text beside it, so a pixel that has been rejecting events since Tuesday is a query, not a discovery.

SELECT event_name, response_status_code, response_body
FROM `your_project.your_dataset.your_table`
WHERE type = 'Response' AND response_status_code >= 400
ORDER BY timestamp DESC
04 What We Changed While Restoring It

Three fixes, all of them visible in the diff.

01Credentials are maskedThe Graph API takes the access token as a query parameter, so the endpoint the original logging recorded carried a live credential into every request row. We mask the token, and the app secret proof when it is enabled, to their first four and last four characters: enough to tell two credentials apart, not enough to use one. Masking happens in one place, so it covers any log point added later. The request itself still carries the real token.
02Failure text is keptThe original failure path wrote its diagnostics to two fields that have no column. Rows are inserted with ignoreUnknownValues, so a timeout landed a row with the diagnostics silently gone and every response field empty. That text goes into response_body now, which is empty on exactly that path.
03Strings are not encoded twiceThe response body arrives as a string and was being JSON-encoded again, which wraps it in escaped quotes and stops JSON_VALUE() reading rows that read fine for every sibling tag writing to the same table.
05 When Upstream Moves

A fetch, a rebase, and one check that guards the point of the repo.

upstream-mirror is a plain mirror of Stape's repository, and we never commit to it. main is that mirror plus the restored logging, because the Community Gallery requires every resource to be on main. Following a release is a fetch, a fast-forward and a rebase.

Continuous integration then checks that the template's embedded JavaScript still matches template.js, and that the logging is still there. That check exists because a rebase is the one way this repository can quietly undo its own reason for existing.

We sync when we want something upstream has shipped, not on a schedule.

If you do not query your tag logs, install upstream's template. It is the same tag with less to configure. This fork is for people who want the send path on the record.

06 Why We Maintain This

A system reporting on its own outbox will always tell you it did its job.

An agency ended up maintaining a tag template.

We run conversion tracking for advertisers, and what we are accountable for is that the numbers a client scales on are true. That is not something you install once. A send path that worked on Monday and is being rejected on Thursday looks the same from inside the container, and for most setups the container is the only witness there is.

So we stopped asking the container. Every tag we run writes what it sent and what came back to a table, and something reads those tables on a schedule instead of waiting for a person to wonder. A tag with no table behind it cannot take part in that: nothing wrote the traffic down on the way past, so there is no going back for it later. That is the whole reason this fork exists, and the reason there will be five more.

If that argument is the interesting part rather than the tag, it is set out at length in the Positive Control, which is the property this logging serves: every conversion watched step by step, browser to our layer to what we sent to what the network confirmed to the CRM, monitored and alerted automatically.