Adds create_giftbit_campaign.js — Node CLI that POSTs to the Giftbit
API (testbed or production), creates a campaign with
delivery_type=SHORTLINK so Giftbit does NOT send their own English
template emails, polls /gifts?campaign_uuid=... until the redemption
shortlinks are generated, then writes a gifts CSV ready to feed into
send_gift_campaign.js.
Two non-obvious things learned while wiring it up:
1. The right endpoint to get the shortlinks is /gifts (not /links).
/links/{uuid} returned 0 rows on our sandbox account; /gifts has
a `shortlink` field on each gift once delivery_status transitions
from QUEUED → LINKCREATED. Polled with 2s interval, up to 20 tries.
2. delivery_type=SHORTLINK is mandatory. Default is GIFTBIT_EMAIL,
which fires their English template immediately — defeating the
whole point of bridging through our French Mailjet template.
Confirmed in the campaign GET response that delivery_type echoes
back correctly when we send "SHORTLINK".
Validated end-to-end (entirely synthetic data — Alice/Bob/Charlie at
@example.com, no real customer info in the sandbox):
✓ Auth probe via /ping returns 200
✓ POST /campaign returns campaign UUID
✓ After ~12s, /gifts returns 3 gifts each with a working shortlink
✓ send_gift_campaign.js consumes the gifts CSV + the contacts CSV
✓ FR template renders: "Bonjour Alice", http://gtbt.co/7TKGFDBNVZq
embedded in the CTA button href, address in the footer line
The --sandbox flag does double duty: routes the API to
api-testbed.giftbit.com AND replaces every recipient email with
louis@targo.ca so we can't accidentally hit real customer inboxes
with the non-redeemable test gifts.
README updated with the two-stage pipeline (create → send), explicit
warnings about the customer-matching gap (only 25% of source rows
resolve via legacy_delivery_id — the rest use a different ID space
from the source Map tool), and the sandbox-quirk where Giftbit
collapses recipient_name when emails are duplicated.
Token NOT committed — pulled from GIFTBIT_TOKEN env var per the
script's contract. In production we'll store it in the hub's
.env alongside SMTP_USER / SMTP_PASS.
|
||
|---|---|---|
| .. | ||
| templates | ||
| contacts_from_legacy.py | ||
| create_giftbit_campaign.js | ||
| README.md | ||
| send_gift_campaign.js | ||
Gift Campaign — Personalized French email sender
One-shot tool to send Giftbit gift cards to a list of contacts with a branded French email, bypassing Giftbit's English-only built-in delivery.
How it works (two-stage pipeline)
The campaign is split into two scripts you run in sequence:
contacts_from_legacy.py # (one-time) extract clean contacts from legacy CSV
↓
contacts.csv
↓
create_giftbit_campaign.js # POST to Giftbit API → SHORTLINK gifts back
↓
gifts.csv + contacts.csv
↓
send_gift_campaign.js # personalized FR emails via Mailjet
↓
results-<timestamp>.csv # per-row status for follow-up
Critical: the create script passes delivery_type=SHORTLINK to Giftbit
so they generate the redemption links but DO NOT send their own English
emails. We then deliver French personalized mail through Mailjet, the
same SMTP wired up for ERPNext invoices.
The Giftbit redemption landing page (where the recipient picks a brand)
is controlled by Giftbit — when creating the campaign through their
dashboard for the first time, set the language to fr-CA so the page
shows in French. The API exposes a language field too but it wasn't
fully exposed on our sandbox account; verify with the campaign you
created in the Giftbit dashboard.
Setup
cd scripts/campaigns
npm init -y # one-time, creates package.json
npm install nodemailer # only dependency (create_giftbit_campaign.js
# uses Node built-ins, no http library needed)
Stage 1 — create the Giftbit campaign
# Sandbox test (all recipients are rerouted to louis@targo.ca for safety):
export GIFTBIT_TOKEN="<your testbed token>"
node create_giftbit_campaign.js \
--contacts ./test-contacts.csv \
--amount-cents 5000 \
--brand-codes amazonca,timhortonsca,walmart \
--expiry 2026-12-31 \
--subject "Cadeau Gigafibre" \
--message "Merci d'être client" \
--sandbox \
--id "test-q4-2026"
# Production (real recipient emails, real gifts charged from your balance):
export GIFTBIT_TOKEN="<your prod token>"
node create_giftbit_campaign.js \
--contacts ./contacts.csv \
--amount-cents 5000 \
--brand-codes amazonca,timhortonsca,walmart \
--expiry 2026-12-31 \
--subject "Cadeau Gigafibre" \
--message "Merci d'être client" \
--id "q4-2026-loyalty"
Output: giftbit-gifts-<id>.csv with columns:
firstname,lastname,email,gift_url,giftbit_uuid,gift_value_cents,internal_id
Alice,Tremblay,louis@targo.ca,http://gtbt.co/7TKGFDBNVZq,bdb28566...,500,TEST-001
internal_id is your contact's account_id column passed through to
join the response back to ERPNext customer records.
The --sandbox flag does TWO things:
- Points the API at
api-testbed.giftbit.cominstead ofapi.giftbit.com - Replaces every recipient email with
louis@targo.caas a safety net so the test gifts (non-redeemable in sandbox) don't actually land in any real customer inbox
Stage 2 — send the personalized French emails
Dry run (no emails sent, HTML written for preview)
node send_gift_campaign.js \
--gifts /path/to/giftbit-gifts.csv \
--contacts /path/to/giftbit-contacts-A-first-email.csv \
--template ./templates/gift-email-fr.html \
--subject "🎁 Un cadeau pour vous, de la part de Gigafibre" \
--amount "50 $" \
--expiry "31 décembre 2026" \
--from "Gigafibre <noreply@gigafibre.ca>" \
--dry-run
A preview-YYYY-MM-DD-HH-MM/ directory will be created with one HTML
file per recipient (numbered + email-suffixed). Open a few in a browser
to validate the rendering on real data, then drop the --dry-run flag
to actually send.
Live send
# Pull SMTP creds from the hub env (same Mailjet account as ERPNext)
source <(ssh root@96.125.196.67 'grep -E "^SMTP_" /opt/targo-hub/.env' | sed 's/^/export /')
node send_gift_campaign.js \
--gifts /path/to/giftbit-gifts.csv \
--contacts /path/to/giftbit-contacts-A-first-email.csv \
--template ./templates/gift-email-fr.html \
--subject "🎁 Un cadeau pour vous, de la part de Gigafibre" \
--amount "50 $" \
--expiry "31 décembre 2026" \
--from "Gigafibre <noreply@gigafibre.ca>" \
--smtp-host in-v3.mailjet.com --smtp-port 587 \
--smtp-user "$SMTP_USER" --smtp-pass "$SMTP_PASS" \
--throttle-ms 600
--throttle-ms 600 = roughly 100 emails/minute, safely below the Mailjet
free-plan ceiling of ~120/min. Adjust upward to 250 ms if you're on a
paid Mailjet plan.
Matching strategies
--match-by |
Behaviour |
|---|---|
row (default) |
Line N of the gifts CSV pairs with line N of the contacts CSV. Use when Giftbit issued the gifts in the same order as your contacts. |
email |
Join by email column present in both CSVs. Use when Giftbit included emails in their export (more robust to ordering mistakes). |
Template variables
The HTML template at templates/gift-email-fr.html uses {{var}} syntax.
Variables resolved at send time:
| Variable | Source |
|---|---|
{{firstname}} |
contacts CSV firstname column (falls back to "cher client") |
{{lastname}} |
contacts CSV lastname |
{{email}} |
contacts CSV email |
{{description}} |
contacts CSV description (we put the service address there) |
{{gift_url}} |
matched from the gifts CSV |
{{amount}} |
--amount CLI flag (e.g. "50 $") |
{{expiry}} |
--expiry CLI flag (e.g. "31 décembre 2026") |
The template uses a vintage {{#expiry}} ... {{/expiry}} block for the
optional expiry line — currently rendered as plain text (the script's
simple {{var}} renderer doesn't strip the tags). If you don't want the
expiry sentence, edit the template directly to remove that block.
Source data — the two CSVs
Contacts (what we send to)
Generated from a service-address selection by scripts/campaigns/contacts_from_legacy.py
(or by hand). One row per recipient:
firstname,lastname,email,description
Marc-André,Boileau,boileau.marcandre@gmail.com,15 Rue des Hirondelles
Maryse,Roy,roy.maryse@hotmail.com,32 Rue des Hirondelles
Gifts (output from Giftbit)
Whatever shape Giftbit gives you. The script auto-detects the URL column from the common naming conventions. Typically:
gift_id,gift_url,amount
gb_abc123,https://app.giftbit.com/g/x7K2N9...,5000
gb_def456,https://app.giftbit.com/g/p2H8M4...,5000
After sending
Check results-<timestamp>.csv:
status=sentrows landed in Mailjet's outbound queue (delivery to the recipient's mailbox is not guaranteed — see Mailjet console for bounces).status=failedrows have the SMTP error in theerrorcolumn. Common causes: malformed email address, hard bounce from a stale legacy email.- Re-run only the failed rows by filtering the results CSV and feeding it back through the script.
What's NOT in this script (intentional MVP scope)
- No persistence to ERPNext doctype (no
Gift Campaignrecords created) - No click tracking — the
gift_urlis included verbatim. Giftbit gives you redemption status via their own API/dashboard. - No ops UI — pure CLI. If we end up running gift campaigns regularly,
wrap this in a
services/targo-hub/lib/gift-campaign.jsendpoint and add a page in ops. For now, one-shot CLI is sufficient.
Known issues to resolve before production
-
Customer matching from the source CSV is only 25% — the
id emplacementcolumn inselectionAdressesMap*.csvis NOT alegacy_delivery_id. Of 216 source rows, only 54 resolve to a Service Location via that column. The other 162 use a different ID space (50000+ range, while migrated SLs are 1-17307). Before going to production, we need to either:- Match by address (street + civic + postal_code) to find the correct Service Location → Customer
- Or have the Map tool include the actual Service Location
name(LOC-XXXXX) in its export The currentaccount_idcolumn in our contacts CSV is approximated; for accurate Customer audit-trail we need this fixed.
-
The Giftbit testbed token in our hub
.envis sandbox-only. Production access requires Giftbit-side KYC + account funding + API approval. While waiting, all testing happens with the testbed token and the--sandboxflag — gift URLs work in their test webapp but represent no real money. -
recipient_namecollapses when emails are duplicated. In sandbox we send all 3 test gifts tolouis@targo.ca, and Giftbit's API returns the samerecipient_namefor all of them (apparently they dedup by email). In production with distinct emails per contact, each gift has the right name. This is a sandbox-only quirk, not a script bug.