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

  1. 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.
  2. We append ?app_account_token=<the token> as a query parameter and send a GET request, with your configured auth header attached.
  3. 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.
  4. 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-token
  • X-API-Key + your-api-key
  • Authorization + 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 to primary_url when both are set.
  • primary_url — string, must start with http:// or https://, 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), and custom_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:

CategoryMeaning
timeoutYour endpoint didn't respond within 5 seconds.
http_NNNYour endpoint returned a non-2xx HTTP status (e.g. http_500, http_403).
invalid_jsonBody wasn't parseable as JSON.
schema_violationJSON parsed but didn't match the schema (e.g. too many fields, a link with no url, a non-http link target).
oversize_responseBody was larger than 8 KiB.
network_errorDNS failure, connection refused, or no route to host.
tls_errorTLS handshake failed (expired cert, etc.).
blocked_hostHostname 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_token against your own database.