← All documents · Download PDF
Verified live 2026-06-30. Every external service the system talks to, and the exact contract for each. Cross-refs: data model
01-data-model.md, flow catalog03-flow-catalog.md, error routing05-error-runbook.md, env/secrets06-env-secrets.md, auditAUDIT-FINDINGS.md. The website (this repo) and n8n (n8n.nieexpertsspain.com) each hold their own credentials — both talk to most of these services.
| Service | Used by | Purpose | Contract anchor |
|---|---|---|---|
| Stripe | website + n8n | Checkout, upsells, refunds, disputes, Connect payouts | Checkout Session id (cs_…) = order id |
| Resend | website + n8n | Transactional email | RESEND_FROM_EMAIL sender |
| Notion | n8n (W15) + website | CRM mirror of orders | Supabase Order ID = cs_… |
| Discord | website + n8n | Ops alerts + typed error routing | webhook URLs per channel |
| Google Drive | n8n | Generated-doc + guide storage/sharing | file/folder ids on practices |
| Cal.com | n8n (W12) | Consultation / group-call booking | booking webhook |
| WhatsApp agent | n8n (W07) | Customer Q&A, escalation, payment links | outbox agent_events |
Checkout. Stripe Checkout Sessions, API version
pinned 2026-04-22.dahlia. Mode is decided by the key:
isTestMode = STRIPE_SECRET_KEY.startsWith('sk_test').
product_data (sandbox products created ad hoc).Order id. The Stripe Checkout Session id
(cs_…) is the canonical order key. It becomes
orders.id and practices.order_id, and in
Notion it is triplicated into Supabase Order ID /
Client ID / Stripe Sessions (H10). There is no
DB-level FK between orders and practices
(AUDIT-FINDINGS H7).
Friend discount.
resolveFriendDiscount() returns exactly
one of two shapes — never both:
discounts: [{ coupon }] — when the order is referred
(server-set nie_affiliate_id), is a base
product, and a matching coupon exists. Coupon is server-resolved, never
client-supplied.allow_promotion_codes: true — otherwise (lets the buyer
type a public promo code).Live friend coupons: B2T1aavc (digital),
ArPaJugz (expert), B1sqQ2mS (concierge-plus,
€10). Test mode uses the sandbox coupon set
(FRIEND_COUPON_*_TEST). Discount values: Digital €5 /
Expert €10 / Concierge⁺ €10 (changed from €20 on 2026-07-03 — commission
stays €20).
Affiliate updates (2026-07-03/04): US ambassadors
supported (accounts created under the FULL service agreement — the
recipient agreement is not offered for US; EEA→US transfers allowed,
~0.25% cross-border fee platform-absorbed). Payout threshold removed
(AFFILIATE_PAYOUT_MIN_EUR default 1). Dashboard: IBAN
button handles add AND update (account_update link for
completed accounts), connected-state copy, package column (Pacchetto) in
the conversions table. Ambassadors receive a commission email on each
referred base order; upsells generate no commission, no ping, no
email.
api/stripe-webhook.jsRaw body, signature-verified with STRIPE_WEBHOOK_SECRET.
Durability rule: it bridges to n8n first (W01 / W01b /
W09) and returns HTTP 500 on a retryable failure before any
non-idempotent side effect, so Stripe retries and orders are
never lost. Side effects run only after the bridge succeeds.
| Stripe event | What the webhook does |
|---|---|
checkout.session.completed |
1) bridge to n8n W01 / W01b / W09 (durable, 500-on-retryable). Then:
2) Notion sync, 3) confirmation email (Resend), 4) Discord
notifyPurchase, 5) affiliate Discord ping (referred
orders), 6) commission ledger row (affiliate_commissions
pending) |
checkout.session.expired |
bridge to W08 (abandoned-checkout handling) |
charge.refunded |
commission clawback + bridge to W16 (refund handler) |
charge.dispute.created |
Notion Dispute status + bridge to W10 (chargeback) |
Christian: a successful purchase is the trigger for everything downstream. If you see a
#stripe-checkoutsDiscord ping but no Notion row within ~5 min, check the n8n W01/W15 executions — the bridge or the sync failed.
api/stripe-connect-webhook.jsSeparate endpoint, separate secret
(STRIPE_CONNECT_WEBHOOK_SECRET). Handles affiliate Connect
account events: | Event | Action | |---|---| |
account.updated | setPayoutsEnabled → writes
affiliates.stripe_payouts_enabled |
maxDuration note: the main webhook runs many sequential
awaits; AUDIT-FINDINGS D4 recommends adding maxDuration: 30
in vercel.json (matches the invoices repo). The n8n bridge
runs first/durably so orders survive, but email/Discord/commission can
be cut on the default timeout.
Sender = RESEND_FROM_EMAIL, default
onboarding@resend.dev — this is the Resend sandbox
sender and MUST change at go-live (AUDIT-FINDINGS B1). If
RESEND_API_KEY is missing, email is
skipped (logged, not thrown).
Test redirect. EMAIL_OVERRIDE_TO
redirects all outbound mail to one inbox (subject
prefixed [TEST→…]). UNSET at go-live (B2).
BCC. admin@nieexpertsspain.com is BCC'd
on the order confirmation and the ambassador
welcome for observability. The magic-link email has NO
bcc (it carries a single-use login token).
Delay. EMAIL_DELAY_MINUTES controls
send delay: order/upsell confirmation default 5 min,
ambassador welcome 0, magic-link never
delayed.
| Trigger | Branding / extras | BCC | |
|---|---|---|---|
| Order / upsell confirmation | checkout.session.completed |
navy/gold IT branding; no booking CTA (separate consulenza invite email since 2026-07-03) | yes |
| Ambassador welcome | affiliate signup approved | referral link nieexpertsspain.com/r/<code> |
yes |
| Magic-link | affiliate/admin login | 15-min portal login token | no |
Bounce webhook (REQUIRED, F2): W05's bounce-recovery
only works if Resend POSTs to
https://n8n.nieexpertsspain.com/webhook/nie-resend-bounce
(events email.bounced, email.complained).
Configure in the Resend dashboard — not verifiable via the send-only API
key.
The n8n side sends via
NIE_LIB_SendEmail (also Resend) with its own test override
— so both halves of the system respect test redirection
independently.
Database "NIE Website Orders", id
36dfd253-5094-80ae-83e3-fdd8a10425c4. Written two ways:
charge.dispute.created.Upsert key = Supabase Order ID (the
cs_… session id).
| Property | Type | Notes |
|---|---|---|
| Cliente | title | currently = email (no real name — H10) |
| Amount EUR | number | |
| People | number | |
| Appointment Qty | number | |
| Tier | select | option duplicates Concierge⁺ vs Concierge+
never match (D1) |
| Stato Pratica | select | New / Welcome Inviato /
Dispute / Rimborsato /
Guida Condivisa |
| Supabase Order ID | text | = cs_… (upsert key) |
| Client ID | text | = same cs_… (triplicated — H10) |
| Stripe Sessions | text | = same cs_… (triplicated — H10) |
| Consulenza / Appuntamento | checkbox | |
| Materiali da Consegnare | formula | empty/dead formula (H10) |
| Created / Last Update | date |
Known issues: Tier select duplicates (D1); title=email + id
triplication + dead formula (H10). See
AUDIT-FINDINGS.md.
Server "NIE Experts Spain". The
website uses three webhooks; n8n uses
its own typed error/event channels (see
05-error-runbook.md).
Website webhooks | Env var | Channel | |---|---| |
DISCORD_WEBHOOK_URL | #stripe-checkouts (also
used for upsell embeds) | | DISCORD_AFFILIATE_WEBHOOK_URL |
#affiliation | | DISCORD_PAYOUT_WEBHOOK_URL |
#affiliation-payout |
| Event | Channel | Webhook |
|---|---|---|
| New purchase | #stripe-checkouts |
DISCORD_WEBHOOK_URL |
| Upsell | #stripe-checkouts (same — title differs only) |
DISCORD_WEBHOOK_URL |
| Referred order | #affiliation |
DISCORD_AFFILIATE_WEBHOOK_URL |
| Weekly payout review | #affiliation-payout |
DISCORD_PAYOUT_WEBHOOK_URL |
AUDIT-FINDINGS H11: landing and upsell share one webhook —
#stripe-checkoutsand#upsell-post-purchaseare not actually separate repo-side; only the embed title differs.
Full channel list (server-wide)
stripe-checkouts,
upsell-post-purchase, new-appointments,
consulenza, affiliation,
affiliation-payout, chargebackmanual-review,
whatsapp-errors, cliente-insoddisfattotechnical-errors,
ex-18-errors, tasa-790-error,
drive-errors, delivery-errors
new-appointmentsis a MANUAL channel, not automated. Christian posts NIE cita booking requests here (name, surname, city, urgency) and the external booking partner ("CITA PARÁ NIE EXTRANJERÍA") books the in-person extranjería appointment. W12's Cal.com consultation/group-call pings deliberately go toconsulenza(not here) so they don't pollute the partner's booking queue — that's why W12'snew-appointmentsnode is disabled. Why it stays manual (by design): the government cita needs human handling — choosing the office and coordinating with the partner — so this channel is intentionally not automated. Optional (Christian's call, not built): a light Notion helper could remove only the copy-paste — Christian fills the cita details on the Notion row, marks it ready, and n8n posts the formatted request here; the human still decides the office/details. Keeping it fully manual is an equally valid — and likely preferred — choice.
Per-practice subfolders. n8n:
NIE_LIB_DriveShare.File and folder ids are persisted on practices
(drive_*_file_id, drive_*_folder_id,
share_permission_id, share_status). Failures
route to #drive-errors (see runbook); W05 attempts recovery
for delivery-side failures.
Booking webhook → W12 records the appointment on the
practice (partner_appointments view surfaces it). The
consultation upsell email carries the Cal.com booking link directly.
Outbound is the outbox pattern: n8n calls
NIE_LIB_NotifyAgent, which enqueues to
agent_events for an external WhatsApp agent (W07
coordinator). The Tally Fase-2 thank-you screen and the agent name
"Milene" hand the customer off to
wa.me/34670557796 for live questions.