Un workflow n8n qui récupère la consommation de crédits de Clay, Apollo et ZoomInfo, la convertit en coût par enregistrement vérifié et publie dans Slack dès que l’une des deux situations coûteuses se produit : la dépense par enregistrement utile qui dérive vers le haut, ou un quota qui part inutilisé vers une date d’expiration. Le bundle dans apps/web/public/artifacts/enrichment-credit-burn-monitor-n8n/ contient l’export n8n complet plus un _README.md couvrant l’import, la configuration des credentials, le tableau des variables d’environnement, la vérification par branche et le coût du monitoring lui-même.
Quand l’utiliser
Utilisez-le quand la dépense d’enrichissement répartie sur deux fournisseurs de données ou plus a dépassé le point où quelqu’un peut encore la garder en tête — en pratique, quand l’engagement annuel cumulé dépasse environ 30 000 dollars et qu’au moins un fournisseur facture sur un cycle que vous pouvez dépasser. À cette taille, les deux modes de défaillance cessent d’être théoriques. Une colonne waterfall ajoute discrètement un fournisseur payant et le coût par enregistrement vérifié grimpe de 40 % sans aucun changement de qualité en sortie. Ou bien le recrutement du T4 glisse, le volume outbound baisse, et 9 000 Clay Actions expirent à la bascule de cycle parce que personne ne surveillait le rythme de consommation face au calendrier.
Il convient aux équipes qui font déjà tourner l’enrichissement par automatisation plutôt qu’à la main — un pipeline d’enrichissement Clay ou une construction de liste ICP — parce que c’est là que part le crédit et là qu’un événement par ligne peut être émis sans plomberie supplémentaire.
Le dénominateur est le point central. Le total de crédits brûlés est un chiffre que votre fournisseur affiche déjà. Les crédits brûlés par enregistrement ayant survécu à la vérification, c’est le chiffre qui vous dit si un changement de fournisseur vous a appauvri, et aucun tableau de bord fournisseur ne le calcule parce qu’aucun fournisseur ne sait lesquelles de vos lignes ont passé votre étape de vérification.
Quand NE PAS l’utiliser
Passez votre chemin si vous n’avez qu’un fournisseur sur un forfait fixe sans dépassement. Un tableau de bord consulté une fois par mois répond à la même question pour un coût de construction nul.
Passez votre chemin si vous n’avez pas d’étape de vérification. Le coût par enregistrement vérifié exige un indicateur de vérification sur chaque ligne : un contrôle de délivrabilité, un filtre catch-all, une colonne de QA. Sans cela, le workflow calcule un coût par enregistrement enrichi, que les fournisseurs publient déjà, et l’alerte de dérive perd son sens.
Écartez spécifiquement la branche d’événements par ligne de Clay si votre volume est élevé et votre besoin d’attribution faible. Chaque événement de ligne coûte 1 Clay Action, parce que les requêtes HTTP consomment des Actions dans le modèle tarifaire Clay de mars 2026. Sur un mois à 20 000 lignes face au quota Growth de 40 000 Actions, instrumenter chaque ligne dépense la moitié du quota pour mesurer l’autre moitié. Instrumentez les deux ou trois tables dont la dépense doit être attribuée et laissez l’export CSV couvrir le reste.
Pourquoi chaque fournisseur est câblé différemment
C’est la partie qui détermine si le workflow rapporte des chiffres ou de la fiction, et cela tient à ce que chaque fournisseur expose réellement.
ZoomInfo est le seul à disposer d’un vrai endpoint d’usage. GET /gtm/data/v1/users/usage renvoie des lignes portant limitType, totalLimit, currentUsage et usageRemaining, où limitType sépare request (débit) de record et uniqueID (ce qui est facturé). Parse ZoomInfo Usage conserve cette séparation et ne valorise que les lignes facturables. La réponse ne porte aucune date de remise à zéro : la fin de contrat relève donc de la configuration, pas de la découverte.
Apollo expose POST /api/v1/usage_stats/api_usage_stats, qui coûte 0 crédit et renvoie des compteurs par endpoint de limit, consumed et left_over sur des fenêtres jour, heure et minute. Ces compteurs comptent des requêtes, pas des crédits. La documentation d’Apollo rend la conversion imprécise dans les deux sens : l’enrichissement de personnes facture 1 crédit pour les données démographiques ou l’email plus 8 de plus quand un mobile est renvoyé, et /people/bulk_match accepte jusqu’à dix personnes par requête. Une requête vaut donc entre 0 et 90 crédits. C’est pourquoi Derive Apollo Credits émet derivedCreditFloor et derivedCreditCeiling et marque le point médian estimated: true, plutôt que de publier un chiffre unique faux et assuré.
Clay ne publie aucun endpoint de solde de crédits. L’usage vit dans Settings → Usage avec des ventilations par workbook et table, par intégration et par signal, plus un export CSV. Le workflow lit cet export depuis une URL que vous contrôlez et tient un ledger parallèle par ligne alimenté par une colonne HTTP API de Clay — ce ledger est le seul endroit où existe un indicateur verified.
Un chiffre du workflow relève d’un choix de modélisation et non d’une donnée fournisseur, et la page ne fera pas semblant du contraire : un abonnement Clay achète les Data Credits et les Actions ensemble, et Clay ne valorise séparément ni l’un ni l’autre. CLAY_DATA_CREDIT_COST_SHARE répartit l’abonnement, par défaut 70/30. Sur Growth (495 dollars par mois pour 6 000 Data Credits et 40 000 Actions, d’après la page tarifaire de Clay vérifiée le 2026-08-05), cela place le Data Credit à 0,0578 dollar et l’Action à 0,0037 dollar. Changez la répartition et tous les montants en dollars en aval bougent avec elle.
Mise en place
Importez le bundle.apps/web/public/artifacts/enrichment-credit-burn-monitor-n8n/enrichment-credit-burn-monitor-n8n.json dans n8n via Workflows → Import from File. Trois points d’entrée : un schedule horaire pour les deux fournisseurs interrogeables par API, un reconcile quotidien à 07:00 pour l’export Clay et les deux prévisions, et un webhook sur /webhook/clay-credit-event pour les événements de ligne.
Réglez le fuseau horaire du workflow. Settings → Timezone (l’export livre America/New_York) avec Execution Order sur v1. Les deux expressions cron lisent ce fuseau, et le déclenchement de 07:00 décide sur quel jour calendaire tombe une bascule de cycle.
Renseignez les dates de cycle.CLAY_CYCLE_END, APOLLO_CYCLE_END et ZOOMINFO_CONTRACT_END, chacune au format YYYY-MM-DD. Aucun fournisseur ici ne renvoie sa propre date de remise à zéro. Laisser l’une vide désactive la prévision d’expiration de ce fournisseur et le signale dans l’item : le workflow ne suppose pas un mois calendaire.
Renseignez les prix unitaires dont vous disposez.APOLLO_CREDIT_USD et ZOOMINFO_RECORD_USD viennent de votre contrat ; aucun des deux fournisseurs ne publie de prix catalogue à l’unité. Non renseignés, les branches de consommation par unité et d’expiration fonctionnent toujours et seule la branche de dérive reste silencieuse.
Câblez deux credentials. Un credential OAuth2 ZoomInfo (PLACEHOLDER_ZOOMINFO_OAUTH2_CRED_ID) et un credential d’app Slack (PLACEHOLDER_SLACK_CRED_ID) avec chat:write. La clé master Apollo vit dans APOLLO_MASTER_API_KEY : l’endpoint usage-stats rejette les clés non master.
Exécutez la vérification en cinq étapes du _README.md avant d’activer le moindre schedule. L’étape 1 est la plus importante : cassez volontairement le credential ZoomInfo et vérifiez que le parser émet ok: false, reason: 'auth_401' au lieu de used: 0.
Modes de défaillance et garde-fous
Un credential expiré se lit comme une dépense nulle. Un 401 transformé en enregistrement d’usage à 0 est pire qu’une panne, parce que chaque seuil en aval le lit comme « la dépense s’est arrêtée » et se tait. Garde-fou : les nodes HTTP tournent avec neverError et fullResponse pour que les codes de statut arrivent au parser comme données, et Parse ZoomInfo Usage émet un enregistrement de santé sur 401, 403 et 429 et aucun enregistrement d’usage.
Un export Clay périmé est indiscernable d’un gel de la dépense. Les deux ressemblent à des chiffres quotidiens plats. Garde-fou : Parse Clay Usage CSV horodate l’âge de l’export et marque stale tout ce qui dépasse CLAY_EXPORT_STALE_HOURS (36 par défaut), ce qui l’oriente vers le canal warning comme message de santé du poll au lieu d’alimenter la ligne de base à 28 jours.
Une colonne d’export renommée met la branche Clay à zéro sans bruit. Les fournisseurs renomment les en-têtes CSV sans préavis. Garde-fou : le parser contrôle les colonnes dont il a besoin et lève une erreur en citant la ligne d’en-têtes réellement lue, au lieu de mettre les colonnes manquantes à 0.
Les petits dénominateurs font exploser le coût par enregistrement vérifié. Douze lignes vérifiées un dimanche calme produisent un chiffre arithmétiquement juste et opérationnellement inutile. Garde-fou : CPVR_MIN_VERIFIED (250 par défaut) doit être atteint dans la fenêtre à 7 jours et dans celle à 28 jours avant que la dérive puisse se déclencher ; en dessous, le node renvoie status: 'insufficient_data'.
Le polling horaire transforme un problème en 24 alertes. Garde-fou : Alert Gate + Dedup indexe sur (kind, vendor, unit, status) dans un bucket de 12 heures stocké dans les static data du workflow. Les static data ne persistent que sur les exécutions de production, raison pour laquelle l’étape de vérification correspondante utilise un schedule activé et non le bouton d’exécution manuelle.
Clay rejoue une colonne HTTP en échec et compte la ligne deux fois. Garde-fou : Clay Ledger Append déduplique sur table::rowId et renvoie duplicate: true à la seconde livraison.
Ce que cela remplace
Le statu quo, ce sont trois tableaux de bord consultés à trois rythmes différents par celui qui y pense. Cela finit par attraper un dépassement et n’attrape presque jamais une expiration, parce que rien dans un tableau de bord fournisseur ne décompte jusqu’à votre bascule de cycle face à votre rythme de consommation.
Un outil de gestion de dépense SaaS (Vertice, Zylo, Cledara) est ici le mauvais instrument, plus qu’un instrument moins bon. Il suit la facture : valeur du contrat, date de renouvellement, nombre de sièges. Il ne voit pas les Data Credits face aux Actions, ne voit pas quel fournisseur d’un waterfall est devenu cher, et n’a pas de dénominateur d’enregistrements vérifiés. Il répond « combien avons-nous payé à Clay » quand ce workflow répond « combien nous a coûté un enregistrement utilisable et le quota va-t-il être gâché ».
Construire la même chose en SQL planifié sur un warehouse est une alternative légitime, et c’est le meilleur choix dès lors que les événements d’enrichissement atterrissent déjà dans le warehouse via votre pipeline. La version n8n l’emporte quand ce n’est pas le cas, parce que le webhook d’événements de ligne et l’ingestion du CSV vous donnent un flux d’événements et une source de rapprochement sans monter d’abord une ingestion.
# Enrichment credit burn monitor — n8n flow
This bundle contains a complete n8n workflow that tracks credit consumption across Clay, Apollo and ZoomInfo, converts it into a cost per verified record, and alerts in Slack on two things that cost money in opposite directions: spend drifting up per useful record, and allowance expiring unused at the end of a billing cycle.
Three entry points:
- **Hourly usage sweep** — `Schedule — Hourly Usage Sweep` fires at the top of every hour and polls the two vendors that expose usage over an API: ZoomInfo (`GET /gtm/data/v1/users/usage`) and Apollo (`POST /api/v1/usage_stats/api_usage_stats`).
- **Daily reconcile** — `Schedule — Daily Reconcile 07:00` pulls the Clay usage export, recomputes cost per verified record against the 28-day baseline, and runs the expiry forecast for every configured cycle.
- **Clay row ledger** — `Webhook — Clay Row Event` accepts `POST /webhook/clay-credit-event`, one call per enriched row from a Clay HTTP API column. This is the only source in the flow that knows whether a row was *verified*, which is the denominator the whole cost model rests on.
## Why the three vendors are handled differently
Only one of the three publishes a usage endpoint, and the design follows from that.
**ZoomInfo** exposes `GET /gtm/data/v1/users/usage` on base `https://api.zoominfo.com/gtm`, returning an array of limit rows with `limitType`, `description`, `totalLimit`, `currentUsage` and `usageRemaining`. `limitType` distinguishes `request` (throughput) from `record` and `uniqueID` (what ZoomInfo bills). `Parse ZoomInfo Usage` keeps that distinction: request limits are emitted as `kind: 'throughput'` and never priced. The response carries no reset date, so the contract-period end has to come from `ZOOMINFO_CONTRACT_END`.
**Apollo** exposes `POST /api/v1/usage_stats/api_usage_stats`, which costs 0 credits and requires a master API key. It returns per-endpoint counters shaped `{ day: { limit, consumed, left_over }, hour: {...}, minute: {...} }` — those are **requests, not credits**. Apollo's documented credit costs make the conversion lossy in both directions: people enrichment charges 1 credit for demographics or email plus 8 more if a mobile phone is returned, and `/people/bulk_match` accepts up to ten people per request. One request is therefore worth anywhere from 0 to 90 credits. `Derive Apollo Credits` emits `derivedCreditFloor` and `derivedCreditCeiling` rather than pretending to a single number, and downstream nodes tag the midpoint `estimated: true`.
**Clay** publishes no credit-balance endpoint at all. Usage lives in Settings → Usage, broken down by workbook and table, by integration, by signal, and with separate tabs for API and MCP spend, with CSV export. `HTTP — Clay Usage Export` reads that export from a URL you control; `Parse Clay Usage CSV` asserts the columns it needs and **throws** if a header is missing, because a silent zero reads downstream as "spend stopped" and suppresses every alert.
## Import
1. In n8n, open **Workflows → Import from File** and select `enrichment-credit-burn-monitor-n8n.json`.
2. Open the workflow's **Settings** and confirm `Execution Order` is `v1` and `Timezone` matches your finance calendar (the export ships `America/New_York`). Both cron expressions are interpreted in this zone, and the daily reconcile's 07:00 firing time determines which calendar day a cycle boundary lands on.
3. Set the environment variables in the table below. The flow starts in a safe state: with no unit prices configured it still reports unit burn and expiry, and only the drift branch goes quiet.
4. Wire the two credentials in the Credentials section.
5. Point one Clay table's HTTP API column at the webhook URL.
6. Run the five-step verification before activating either Schedule Trigger.
## Credentials
### 1. ZoomInfo GTM Data API (OAuth2) — `PLACEHOLDER_ZOOMINFO_OAUTH2_CRED_ID`
Used by `HTTP — ZoomInfo Usage`. Create a **Generic Credential Type → OAuth2 API** credential in n8n and attach the scopes your contract includes (`api:data:company`, `api:data:contact`, `api:data:intent`, `api:data:news`, `api:data:scoops`, `api:data:lookup`). Client credentials come from **Admin Portal → Integrations → API & Webhooks**.
If your contract is still on the legacy Enterprise API rather than the GTM Data API, the auth model is different: `POST https://api.zoominfo.com/authenticate` with username and password, or a client ID plus private key pair, returns a JWT valid for **one hour**, passed as `Authorization: Bearer <jwt>` on every subsequent call. That flow needs a `Set` node plus an HTTP node in front of the usage call and a re-auth on every execution — an hourly schedule means the token is expired at every firing, so caching it buys nothing. Rate limits on that API are 1 request per second on the authentication endpoint and 25 per second (1,500 per minute) on the standard endpoints.
### 2. Slack — `PLACEHOLDER_SLACK_CRED_ID`
Used by `Slack — Notify`. A Slack app credential with `chat:write` and, if you post to channels the bot is not a member of, `chat:write.public`. Invite the bot to both channels named in `SLACK_CHANNEL_CRITICAL` and `SLACK_CHANNEL_WARNING`.
### 3. Apollo master API key — env var, not a credential
`HTTP — Apollo Usage Stats` sends `x-api-key: {{ $env.APOLLO_MASTER_API_KEY }}`. The usage-stats endpoint needs a **master** key (OAuth scope `api_usage_stats_read`); an ordinary key returns 403. It is kept in an env var rather than an n8n credential so the same key can be read by the derivation node's cost table without a second credential lookup.
### 4. Clay export URL — env var
`CLAY_USAGE_EXPORT_URL` should point at a signed URL or object-store path holding the most recent CSV export from Settings → Usage. Clay has no API for this, so the export is either a manual weekly drop or a small scheduled job of your own. Whichever it is, the freshness guard below matters more than the mechanism.
## Environment variables
| Variable | Default | What it does |
|---|---|---|
| `APOLLO_MASTER_API_KEY` | — | Master key for the usage-stats endpoint |
| `APOLLO_CYCLE_END` | — | `YYYY-MM-DD` end of the Apollo billing cycle. Unset disables the Apollo expiry forecast rather than guessing it |
| `APOLLO_CREDIT_ALLOWANCE` | — | Credits granted this cycle, from your plan |
| `APOLLO_CREDIT_USD` | — | Your contracted dollars per credit. No public list price exists per plan tier, so leaving this unset is the honest default; unit burn still reports |
| `ZOOMINFO_CONTRACT_END` | — | `YYYY-MM-DD` end of the contract period. The usage response has no reset date |
| `ZOOMINFO_RECORD_USD` | — | Contracted dollars per returned record. ZoomInfo does not publish pricing |
| `CLAY_USAGE_EXPORT_URL` | — | Where the Settings → Usage CSV export lands |
| `CLAY_EXPORT_STALE_HOURS` | `36` | Older than this and the snapshot is tagged `stale` and routed away from the cost maths |
| `CLAY_PLAN_MONTHLY_USD` | `495` | Clay plan fee used to price units. Growth is $495/mo billed monthly |
| `CLAY_DATA_CREDIT_ALLOWANCE` | `6000` | Data Credits per month on your plan |
| `CLAY_ACTION_ALLOWANCE` | `40000` | Actions per month on your plan |
| `CLAY_DATA_CREDIT_COST_SHARE` | `0.7` | Share of the Clay plan fee attributed to Data Credits rather than Actions |
| `CLAY_DC_ROLLOVER_MULTIPLE` | `2` | Data Credit rollover cap as a multiple of the monthly allowance |
| `CLAY_CYCLE_END` | — | `YYYY-MM-DD` end of the Clay billing cycle |
| `CPVR_DRIFT_PCT` | `25` | Trailing-7-day cost per verified record above the 28-day baseline by this percentage fires a drift alert |
| `CPVR_MIN_VERIFIED` | `250` | Minimum verified records in both windows before drift can fire |
| `FORFEIT_ALERT_PCT` | `15` | Projected unused share of an allowance that triggers a forfeit alert |
| `CYCLE_WARN_DAYS` | `7` | Days before cycle end that the forfeit alert becomes eligible |
| `ALERT_DEDUP_HOURS` | `12` | Dedup bucket width |
| `SLACK_CHANNEL_CRITICAL` | `#revops-alerts` | Drift and overrun |
| `SLACK_CHANNEL_WARNING` | `#revops-costs` | Forfeit and poll-health |
### The one number that is a modelling choice, not a vendor fact
`CLAY_DATA_CREDIT_COST_SHARE` exists because a Clay plan fee buys Data Credits and Actions together and Clay does not price them separately. At the shipped default — Growth at $495 per month, 70% attributed to 6,000 Data Credits and 30% to 40,000 Actions — a Data Credit costs $0.0578 and an Action costs $0.0037. Move the share and both move. Anyone reading the resulting cost per verified record should know it inherits that assumption; if your spend is overwhelmingly one unit, set the share to match and note it where the number is reported.
Clay's published plan anchors, checked 2026-08-05: Free 100 Data Credits and 500 Actions; Launch $185/mo for 2,500 Data Credits and 15,000 Actions; Growth $495/mo for 6,000 and 40,000. Annual billing is roughly 10% lower per month ($167 and $446).
## Clay row events
Point a Clay HTTP API column at `POST https://<your-n8n>/webhook/clay-credit-event` with a JSON body:
```json
{
"rowId": "{{row_id}}",
"table": "outbound-icp-q3",
"workbook": "GTM",
"integration": "prospeo",
"dataCredits": 2,
"actions": 1,
"verified": true
}
```
`rowId` is required and is the dedup key — Clay retries failed HTTP columns, and `Clay Ledger Append` will otherwise double-count the retry. `verified` should be the output of whatever verification step you already run (a deliverability check, a catch-all filter, a manual QA column), not the enrichment's own "found" flag. The ledger is a 45-day ring buffer in `$getWorkflowStaticData('global')`.
## What the monitor itself costs
Worth knowing before you activate it, because two of the three branches are not free:
- **ZoomInfo**: one usage call per hour, 24 per day. It counts against the `request` limit, not the `record` limit, so it consumes throughput, not credits.
- **Apollo**: `api_usage_stats` costs 0 credits. It does count against that endpoint's own rate limit.
- **Clay**: each row event costs **1 Action**, because HTTP requests consume Actions under Clay's March 2026 pricing model. On a 20,000-row month at the Growth allowance of 40,000 Actions, instrumenting every row spends 20,000 Actions — half the allowance — to measure the other half. Instrument the tables whose spend you actually need to attribute, not all of them. The CSV export path costs nothing and covers the rest.
## First-run verification
Run these in order, before activating either schedule. Each one proves a branch in isolation.
1. **ZoomInfo parse, auth-failure path.** Temporarily break the OAuth2 credential (change the client secret to `x`) and execute `HTTP — ZoomInfo Usage` manually. Expect one item with `ok: false, reason: 'auth_401'` and **no** usage records. If you see `used: 0` instead, the `neverError` option was lost in import — restore it, or an expired credential will silently read as zero spend forever.
2. **ZoomInfo parse, success path.** Restore the credential and re-run. Expect one item per limit row, with `billable: true` only on `record` and `uniqueID` rows.
3. **Apollo bracket.** Execute `HTTP — Apollo Usage Stats` then `Derive Apollo Credits`. Confirm `derivedCreditFloor` is at or below `derivedCreditCeiling` and that `perEndpoint` lists only the credit-consuming endpoints — record management endpoints should be absent, since they cost 0 credits.
4. **Clay export guard.** Point `CLAY_USAGE_EXPORT_URL` at a CSV whose header row you have deliberately renamed (`credits` → `credit_total`) and execute `Parse Clay Usage CSV`. Expect a thrown error naming the headers it saw. Restore the real export and confirm you get one item per day with `stale: false`.
5. **Ledger and dedup.** POST the sample body above to the webhook twice with the same `rowId`. The first returns `duplicate: false`, the second `duplicate: true`, and the ledger holds one entry. Then activate `Schedule — Daily Reconcile 07:00` and let it fire twice within the dedup window: the second firing should post nothing to Slack. Static data does not persist across manual executions, so this last check only works with an activated schedule.
## Watch-outs
**Drift alerts are meaningless on small denominators.** Twelve verified rows on a quiet Sunday produce a cost per verified record that is arithmetically correct and operationally useless. `CPVR_MIN_VERIFIED` defaults to 250 verified records in *both* windows; below that the node returns `status: 'insufficient_data'` and no alert fires. Raise it if your weekly volume is high enough to support it.
**A stale Clay export looks exactly like a spend freeze.** Both produce flat daily numbers. `CLAY_EXPORT_STALE_HOURS` tags snapshots older than 36 hours, and the alert gate routes those to the warning channel as a poll-health message instead of feeding them into the baseline.
**The Apollo number is a bracket, not a measurement.** If your Apollo usage is mostly `bulk_match`, the ceiling can be an order of magnitude above the floor. When the split matters, ledger the Apollo calls at their call sites the same way the Clay column does — the usage endpoint cannot get you there on its own.
**Cycle-end dates are configuration, not discovery.** No vendor in this flow returns its reset date. Every unset `*_CYCLE_END` disables that vendor's expiry forecast and says so in the item, rather than assuming a calendar month. Diarise updating them when a contract renews.
**Static data is per-workflow and not backed up.** Re-importing the workflow as a new record starts the ledger and the dedup map empty, which means a fresh 28-day baseline. Export before you re-import if the history matters.