Skip to main content

Embedded purchase flow

The embedded flow puts Rhino's purchase experience inside an iframe in your application, so the renter never leaves your product. The same iframe URL serves Security Deposit Alternative or Renter Guarantee — the product comes from the prospect (guarantor_coverage), not from a different URL. Rhino handles the application, underwriting, disclosures, payment, and policy issuance.

You do not construct the iframe URL yourself. Get a prospect into Rhino — by emitting partner webhook events or through the Partner Prospect API — and Rhino produces the enrollment_url you load in the iframe. It reaches you on the prospect.ready webhook, or in an API response if you are polling.

How it works

  1. Your backend creates a prospect and receives an enrollment_url.
  2. The renter chooses Rhino in your product.
  3. Your page loads the enrollment_url in an iframe.
  4. The renter completes the application and pays, inside the iframe.
  5. The iframe posts completed to your page.
  6. Rhino sends your backend a policy.created webhook.

The postMessage in step 5 tells your UI what to do next. The webhook in step 6 is the authoritative record. Do not treat a postMessage as proof a policy exists — the renter's browser can close before the message is sent, and the message carries no verifiable data.

What the renter does inside the iframe

Step 4 above is several screens. Knowing the sequence helps when you are sizing the frame or explaining the handoff to your own users:

  1. Create an account or log in. This is the first screen, and it is required — Rhino needs an authenticated renter to attach a policy to. Renters who already have a Rhino account log in here instead.
  2. Confirm their details. Whatever you sent on the prospect is prefilled, so the more complete your data, the shorter this step.
  3. Underwriting. Runs inline, and is where a decline happens.
  4. Review the quote. Pricing appears here for the first time.
  5. Pay.
  6. Confirmation.

The same iframe sequence applies to both Security Deposit Alternative and Renter Guarantee. The screens differ once pricing appears.

Account creation happens in the frame rather than out of band, so do not build a flow that assumes the renter arrives already authenticated with Rhino.

Enrollment URL structure

https://www.sayrhino.com/enrollment/{flow}/{property_owner_slug}/{source}/{source_prospect_id}?email={email}
SegmentMeaning
flowembedded for iframe integrations, expedited for full-page redirects
property_owner_slugThe property owner the prospect belongs to
sourceYour partner source code, assigned by Rhino
source_prospect_idThe prospect ID you supplied
emailRequired. Must match the prospect's email or the page returns 403.

Use the URLs from the API response rather than assembling this yourself. enrollment_url carries the flow your source is configured for; hosted_enrollment_url is the same enrollment on expedited, for a renter you cannot iframe. To change which flow your source defaults to, talk to your Partner Success Manager.

Optional query parameters

Append these to the enrollment URL to skip ahead or control the return experience:

ParameterDescription
selected_productsecurity_deposit_insurance or cash_deposit. Preselects the product. Ignored if the renter has already chosen one. Defaults to security_deposit_insurance. Renter Guarantee partners should use the enrollment_url Rhino returned; do not assemble selected_product=renter_guarantee_insurance.
cadenceupfront or monthly, for SDA only. Defaults to upfront. Renter Guarantee is always upfront.
redirect_urlWhere to send the renter after submission. Silently ignored unless the host matches the domain Rhino has allowlisted for your source.

The prospect API also returns ready-made per-product URLs in offered_products (upfront_enrollment_url, monthly_enrollment_url, enrollment_url) with these parameters already applied. Those appear only after the renter has been underwritten in the flow, and only for SDA and cash deposit — there is no Renter Guarantee row. They are for sending someone back to a specific choice, not for their first visit. They carry your source's configured flow segment and have no hosted variant, so do not open one top-level if your source is embedded — swap the segment for expedited yourself, as hosted_enrollment_url does.

Personal details such as first_name, last_name, and move_in_date are not accepted as URL parameters. Send them on the prospect record instead — the flow reads them from there.

Embedding

<iframe
id="rhino-iframe"
src="https://www.sayrhino.com/enrollment/embedded/acme-properties/acme/prospect-123?email=renter%40example.com"
width="100%"
height="800"
frameborder="0"
allow="payment"
sandbox="allow-scripts allow-same-origin allow-forms">
</iframe>

allow="payment" is required. Rhino's checkout uses Stripe, which runs its own nested iframe and needs access to the Payment Request API. Without this attribute the renter reaches checkout and cannot pay.

If you sandbox the iframe, these three flags are the minimum and each one breaks something specific if omitted:

FlagNeeded for
allow-scriptsThe flow itself, and postMessage
allow-same-originSession cookies. Without it the renter is logged out between steps.
allow-formsForm submission during application and checkout

Do not add further sandbox flags without checking with Rhino first. If you are not already sandboxing iframes, omitting the attribute entirely is fine.

Allowlisting

Rhino must allowlist your domains before the iframe will render or send events. One setting is configured per property owner during onboarding:

  • embedded_frame_ancestors — the origins permitted to frame the flow and receive postMessage events. Rhino sends Content-Security-Policy: frame-ancestors 'self' <your origins> and strips X-Frame-Options on embedded routes.

Send your Partner Success Manager every staging and production parent origin. Rhino resolves the actual parent and sends events only when it exactly matches an entry or is covered by a configured subdomain wildcard. A wildcard such as https://*.example.com covers https://staging.example.com, but not https://example.com.

Give each entry as a bare origin — scheme included, no trailing slash and no path:

https://app.example.com correct
https://app.example.com/ wrong
https://app.example.com/enroll wrong

Tell Rhino before your origins change. A renter hitting a stale allowlist gets a blank frame with only a console error to explain it.

The flow relies on cookies for session state, so third-party cookies must be permitted for sayrhino.com in the framing context.

Window events

When the flow reaches a terminal state, Rhino resolves the actual parent origin, confirms that it is allowlisted, and calls window.parent.postMessage(event, parentOrigin). If the parent cannot be resolved or is not allowlisted, no event is sent.

The message payload is a plain string, not an object. There is no event.data.event property, no policy ID, and no timestamp.

EventMeaning
completedThe renter finished and a policy or cash deposit was created
declinedUnderwriting declined the application. Declines are final — do not send the renter back in.
blockedThe flow cannot continue — ineligible renter, pricing failure, or a configuration problem
window.addEventListener('message', (event) => {
if (event.origin !== 'https://www.sayrhino.com') {
return;
}

switch (event.data) {
case 'completed':
showSuccessState();
break;
case 'declined':
showDeclinedState();
break;
case 'blocked':
showFallbackDepositOptions();
break;
default:
break;
}
});

Keep the default case. Rhino's emitter also recognizes a fourth name, opt_out, which no screen currently sends but which may start appearing without notice. Ignore values you do not recognize rather than throwing.

Every event is fire-and-forget. Rhino expects no acknowledgement and does not retry, and the iframe does not close or navigate itself — moving the renter on is your job.

Some terminal screens emit on page load; others emit when the renter clicks the closing call to action, so a completed message may arrive a few seconds after the policy is actually issued. Always validate the outcome against the policy.created webhook or a GET on the prospect before granting anything of value.

Security

Validate the origin

Always compare event.origin against an explicit allowlist. Any page can post a message to your window.

const allowedOrigins = [
'https://www.sayrhino.com',
'https://www.stage.sayrhino.com',
];

if (!allowedOrigins.includes(event.origin)) {
return;
}

Content Security Policy

If your app sets a CSP, permit the Rhino frame:

Content-Security-Policy: frame-src https://www.sayrhino.com https://www.stage.sayrhino.com;

Layout

The flow is responsive, but an iframe cannot resize itself to its content. Give it enough height to avoid a nested scrollbar on the longest step:

#rhino-iframe {
width: 100%;
min-height: 800px;
border: 0;
}

@media (max-width: 768px) {
#rhino-iframe {
min-height: 1000px;
}
}

Testing

Use the staging environment at https://www.stage.sayrhino.com. Staging enrollment routes skip the HTTP basic auth that guards the rest of the staging site, so the iframe loads without credentials.

Worth exercising before launch:

  1. A prospect that reaches ready and completes a purchase
  2. A prospect that stays incomplete because of a missing field
  3. A declined application
  4. A renter who abandons mid-flow and returns to the same URL
  5. Mobile viewport rendering

Troubleshooting

The iframe renders blank or is refused

  • Confirm your domain is in embedded_frame_ancestors for the property owner
  • Check the browser console for a frame-ancestors CSP violation
  • Confirm third-party cookies are not being blocked

403 or "Third party prospect not found"

  • The email query parameter is required and must match the prospect's email exactly (comparison is case- and whitespace-insensitive, but the value must be present)
  • Confirm the source and source_prospect_id in the path match the prospect you created

No postMessage events

  • Confirm the actual parent origin is listed in embedded_frame_ancestors or covered by a configured wildcard
  • Confirm its HTTPS scheme and port match the allowlist entry
  • Confirm you are reading event.data as a string rather than event.data.event
  • Confirm your listener is attached before the iframe finishes loading

The renter is logged out between steps, or a form will not submit

  • You are sandboxing the iframe without allow-same-origin (session) or allow-forms (submission)

The renter reaches checkout but cannot pay

  • The allow="payment" attribute is missing, so Stripe's nested frame cannot reach the Payment Request API

Next steps