NIE EXPERTS SPAIN

Integrations

System Documentation · Node Agency

04 · External integrations

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 catalog 03-flow-catalog.md, error routing 05-error-runbook.md, env/secrets 06-env-secrets.md, audit AUDIT-FINDINGS.md. The website (this repo) and n8n (n8n.nieexpertsspain.com) each hold their own credentials — both talk to most of these services.

Integration map

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

Stripe

Checkout. Stripe Checkout Sessions, API version pinned 2026-04-22.dahlia. Mode is decided by the key: isTestMode = STRIPE_SECRET_KEY.startsWith('sk_test').

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:

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.

Main webhook — api/stripe-webhook.js

Raw 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-checkouts Discord ping but no Notion row within ~5 min, check the n8n W01/W15 executions — the bridge or the sync failed.

Connect webhook — api/stripe-connect-webhook.js

Separate 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.


Resend (email)

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.

Email 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.


Notion (CRM)

Database "NIE Website Orders", id 36dfd253-5094-80ae-83e3-fdd8a10425c4. Written two ways:

Upsert key = Supabase Order ID (the cs_… session id).

Property Type Notes
Cliente title currently = email (no real name — H10)
Email email
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.


Discord (ops alerts)

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 |

Discord routing (website side)

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-checkouts and #upsell-post-purchase are not actually separate repo-side; only the embed title differs.

Full channel list (server-wide)

new-appointments is 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 to consulenza (not here) so they don't pollute the partner's booking queue — that's why W12's new-appointments node 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.


Google Drive

Per-practice subfolders. n8n:

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.


Cal.com

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.


WhatsApp

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.