# Hyzl setup for {{APP_NAME}}
Add one server event when someone leaves the paywall so Hyzl can check payment, wait, and follow up. Make the change, but stop before anything could text a real customer.
## 1. Connection details
- App: {{APP_NAME}} (id `{{APP_ID}}`)
- Endpoint: `POST {{WEBHOOK_URL}}`
- Auth header: `Authorization: Bearer {{INGEST_TOKEN}}`
- Header: `Content-Type: application/json`
- Store the key as `HYZL_INGEST_TOKEN` on the server. Never put it in code, logs, client apps, screenshots, or chat.
- Provider: {{PROVIDER}} · Link mode: {{LINK_MODE}}{{PAYWALL_URL_LINE}}
## 2. Rules
- Send the event from the server only. The ingest key must never reach a browser or mobile app.
- `consent.attested` is the person's real choice. Never hardcode `true`. Omitting consent preserves the existing choice; sending `false` revokes it and stops outreach.
- In development, use `test: true` only with your own controlled account and phone number. Production events omit `test` or set it to `false`; after deployment they start recovery automatically when the app is Live. Never test on a real customer.
- Do not slow or break the user's flow. Send in a background job after the response, time out after 10 seconds, and log only the HTTP status and Hyzl `error` code.
## 3. Where to fire
- Send when a signed-in person sees the price and leaves without paying. If leaving cannot be detected reliably, send when the paywall renders; Hyzl's wait absorbs people who are still deciding.
- Never fire at signup or when a phone number is first collected.
- Use a durable identity of at least five characters. For RevenueCat, `userId` must exactly match the value passed to `Purchases.logIn()`. For Stripe, send the `cus_…` customer id when known and set Checkout `client_reference_id` or `metadata.userId` to the same `userId`. Email or phone alone is rejected.
- For Stripe recovery links, set Stripe `metadata.source=hyzl` or `metadata.campaign` to a value beginning with `hyzl` on the Checkout Session, Subscription, or Invoice. Unmarked payments and RevenueCat purchases appear as paid-after-contact, not recovered revenue.
## 4. Phone field and consent
Reuse any consent flow already in the app and send its real state. If none exists, add a field labeled **Phone number (optional)** and an unchecked checkbox with this exact wording:
> I agree that {{APP_NAME}} and its service providers may contact me by phone call and text message at the number provided, including with automated or AI-assisted technology, about my account and related offers. An AI may answer calls, and calls may be recorded and transcribed. Consent is not a condition of purchase. Message frequency varies. Message and data rates may apply. Reply STOP to opt out. Reply HELP for help. {Terms link} · {Privacy link}
Replace the placeholders with the app's real Terms and Privacy Policy links. Keep the phone field optional and the checkbox clear by default. Store the wording shown and the time it was ticked.
## 5. The event
Send this JSON from a server-side function such as `sendHyzlPaywallEvent`. Replace the examples with real values; `consent.attested` must remain a JSON boolean. Omit unavailable optional fields, including `recoveryUrl`, instead of sending placeholder strings:
```json
{
"userId": "<durable user id>",
"stripeCustomerId": "<cus_… when Stripe and known>",
"phoneNumber": "<E.164, for example +13105551212>",
"firstName": "<first name if known>",
"email": "<email exactly as stored, if known>",
"consent": {
"attested": false,
"text": "<wording shown>",
"at": "<ISO 8601 time when ticked>"
},
"eventId": "<stable id for this paywall exit>",
"occurredAt": "<ISO 8601 or epoch milliseconds>",
"stage": "saw_price",
"recoveryUrl": "<https link when using a per-person link>"
}
```
- Normalize the phone to E.164 or do not send it. A later event may add the phone and consent, using the same `userId` and a new `eventId`.
- Build a stable `eventId` from the user id plus the paywall session or UTC date. Retrying the same id returns `200` without reprocessing event details; `optedOut: true` still records STOP and withdraws contact. A later update needs a new id.
- `occurredAt` accepts ISO 8601 or epoch milliseconds. Use a 10-second timeout. Treat `200` and `202` as success. Fix `4xx`; retry only network errors and `5xx`.
- Optional `optedOut` is a boolean. `true` records a permanent STOP for this app and phone. Send `userId` or `stripeCustomerId`, a valid E.164 `phoneNumber`, `consent.attested: false`, and `consent.voice.attested: false`. A checkbox withdrawal remains reversible and needs no `optedOut`. Omitting `optedOut` or sending `false` does not clear a stored STOP.
- Optional fields: numeric `price` with its ISO `currency`; `context` with factual context up to 1000 characters; `detail` with a link-free line up to 80; and integer `percentOff` from 1–90. In `per_person` link mode, `offerLinks` maps integer percents 1–90 to your own links. `recoveryUrl` and offer links must be https, contain no credentials, and be at most 2048 characters.
## 6. Test, then report
1. Send one explicitly invoked `test: true` event for a controlled identity and phone number you own, with `consent.attested: true` after giving the same real consent described above. Expect `202`. This bypasses the app status, wait, and paid check. A `202` means accepted, not delivered, so do not wait for a text or block the handoff.
2. Send one normal event for a different controlled identity. Expect `202` with a status. Nothing is texted until the normal wait passes and the app is live.
3. Use the `202` response to confirm Hyzl stored each event. Do not remint the key, resend blindly, or ask the merchant for another credential.
4. Report the files changed, trigger location, identity and consent sources, the token variable name (never its value), open questions, and which environments still need `HYZL_INGEST_TOKEN`.
## 7. Responses and what Hyzl does next
```text
202 stored: person_id, status, contactable, consent_attested, next_action_at
200 duplicate eventId: no event reprocessing; optedOut: true still records STOP and withdraws contact
200 no_person_to_withdraw: outcome ignored; no lead is created; optedOut: true still records a permanent STOP for this app and phone
200 support_contact: outcome ignored; the user or phone belongs to a support conversation from the Inbox; no lead or recovery; optedOut: true still records STOP; consent.attested: false still withdraws consent, even with a repeated eventId
400 malformed_body | invalid_phone | invalid_consent | missing_durable_id
400 invalid_opt_out: use a boolean; true requires a valid E.164 phoneNumber, consent.attested: false and consent.voice.attested: false
400 invalid_recovery_url | invalid_percent_off | invalid_offer_links
400 invalid_context | invalid_detail | invalid_occurred_at | test_requires_phone
401 unauthorized: missing or wrong token
405 method_not_allowed: use POST
500 internal_error: retry
```
In Auto mode, Hyzl chooses the next text, call, wait, or stop; in Flow mode, it follows the saved sequence. Before contact Hyzl checks payment and consent. A reply pauses scheduled Flow steps, payment or STOP ends outreach, quiet hours apply, and outbound calls are off by default.