Customer user lookup
Most teams want notifications about a subscription event to include the customer's own identity — their name, their plan, a link to their page in your admin dashboard. Juice Machine can call a URL of your choice for each event that carries an appAccountToken, merge the response into the notification, and surface the fields in every channel.
How it works
- When a webhook event from Apple includes an
appAccountToken(the UUID your app sets at purchase time via StoreKit), Juice Machine looks up the URL you've configured on the app. - We append
?app_account_token=<the token>as a query parameter and send a GET request, with your configured auth header attached. - Your endpoint returns JSON describing the matching user. We validate it against a strict schema (limits below), persist the result on the event, and forward it to every configured destination.
- Slack / Microsoft Teams / Discord / email / PagerDuty / Telegram / generic-webhook notifications all surface the user fields in their native shape.
Configuration
On your app's edit page (Dashboard → your app → Edit), find the Customer user lookup section:
- Lookup URL — your base endpoint. We append
?app_account_token=…at request time. Existing query parameters on the URL are preserved. - Auth header name (optional) — e.g.
Authorization,X-API-Key. - Auth header value (optional) — encrypted at rest. Never displayed back to the browser; an empty submission means "leave the stored value alone".
Auth pair examples that cover ~99% of customer setups:
Authorization+Bearer your-tokenX-API-Key+your-api-keyAuthorization+Basic base64(user:pass)(you encode it; we send it verbatim)
Try it without writing any code
Want to see the feature working end-to-end before you build your own endpoint? Point your lookup URL at our sample data endpoint:
https://juicemachine.net/sample-user-lookup
It returns a fixed sample payload (with whatever app_account_token we passed reflected back under a "Token" field) that exercises every supported response field. Trigger a webhook with an appAccountToken set, watch the notification land with the sample user fields in it, then swap the URL to your own endpoint when you're ready.
Response schema
Your endpoint returns JSON in this shape:
{
"name": "Jane Doe",
"primary_url": "https://your-app.example.com/admin/users/abc123",
"fields": [
{ "label": "Email", "value": "jane@example.com" },
{ "label": "Plan", "value": "Pro (annual)" },
{ "label": "MRR", "value": "$49" },
{ "label": "Signed up", "value": "March 2024" }
],
"links": [
{ "label": "Subscription", "url": "https://your-app.example.com/admin/users/abc123/subscription" },
{ "label": "Support tickets", "url": "https://your-app.example.com/admin/users/abc123/tickets" }
]
}
Every key is optional. Limits enforced server-side:
name— string, max 100 chars. Rendered as the bold header of the customer section in each channel; linked toprimary_urlwhen both are set.primary_url— string, must start withhttp://orhttps://, max 2,000 chars. The "canonical" link for the customer — typically their page in your admin dashboard.fields— array of{ "label", "value" }hashes, max 8 entries. Labels capped at 30 chars, values at 200. Rendered as a 2-column key/value list (Slack), FactSet (Teams), inline embed fields (Discord), table (email), andcustom_details(PagerDuty).links— array of{ "label", "url" }hashes, max 4. Same character caps as fields. Rendered as action buttons (Slack), markdown links (Teams, Discord, Telegram), a "Subscription · Tickets · …" row (email), and additional links on the incident (PagerDuty).
Total response body must be ≤8 KiB. The full schema is enforced by UserLookupCaller at runtime; any violation falls back to rendering the notification without user fields, with an error category surfaced on the event detail page.
Timeouts, errors, and retries
Hard timeout is 5 seconds (connect + read). Anything that fails — timeout, non-2xx response, malformed JSON, schema violation, network error — falls back to the notification being delivered without user fields. The delivery itself never blocks on the lookup; your team always gets the notification.
When something fails, the event's detail page in your dashboard surfaces a short error category so you can debug:
| Category | Meaning |
|---|---|
timeout | Your endpoint didn't respond within 5 seconds. |
http_NNN | Your endpoint returned a non-2xx HTTP status (e.g. http_500, http_403). |
invalid_json | Body wasn't parseable as JSON. |
schema_violation | JSON parsed but didn't match the schema (e.g. too many fields, a link with no url, a non-http link target). |
oversize_response | Body was larger than 8 KiB. |
network_error | DNS failure, connection refused, or no route to host. |
tls_error | TLS handshake failed (expired cert, etc.). |
blocked_host | Hostname resolved to a private / loopback / link-local IP and was rejected for SSRF safety. |
Lookups are not automatically retried, and re-deliveries reuse the cached result rather than re-calling your endpoint. If you want to refresh user data on an existing event, change the response on your end and trigger a fresh webhook from Apple (e.g. by making a sandbox purchase) — the cached result is per-event, not per-token.
Security
- The auth header value is encrypted at rest using ActiveRecord Encryption, same as your App Store Connect private key.
- SSRF protection: the configured URL must resolve to a public IP. We reject loopback (127.0.0.0/8, ::1), RFC 1918 private ranges, link-local (169.254.0.0/16), and IPv4-mapped IPv6 addresses. DNS is re-resolved at request time so a rebinding attack between save and call still gets caught.
- Your endpoint is your infrastructure, not a sub-processor of Juice Machine. The customer fields you return are entirely under your control; we never inspect them beyond schema validation.
What we don't do (yet)
- Avatars. Slack, Teams, and Discord all support inline images, but we don't surface a user-avatar URL yet — the schema may grow this later.
- Cache TTL. Every event calls your endpoint fresh. If your customer-store load is a concern, cache on your side (and use HTTP caching headers — we'll respect them in a future version).
- Multi-header auth. A single auth header pair covers Bearer, API key, and Basic auth. Setups requiring two headers (e.g. Cloudflare Access service tokens) aren't supported yet.
- Webhook signing. We don't sign the GET request, so your endpoint can't currently verify that the request came from Juice Machine. Mitigations: use your auth header, allowlist our IPs at your edge, or validate the
app_account_tokenagainst your own database.