Skip to main content

Embedded SDA Flow

The embedded flow puts Rhino's Security Deposit Alternative purchase experience inside an iframe in your application, so the renter never leaves your product. 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.

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.

The flow segment is determined by your source configuration in Rhino, not chosen per request. If you need to switch between embedded and expedited, talk to your Partner Success Manager. Use the enrollment_url from the API response rather than assembling this yourself.

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.
cadenceupfront or monthly, for SDA only. Defaults to 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, so they are for sending someone back to a specific choice, not for their first visit.

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 domain before the iframe will render. Two things are configured per property owner during onboarding:

  • embedded_frame_ancestors — the domains permitted to frame the flow. Rhino sends Content-Security-Policy: frame-ancestors 'self' <your domains> and strips X-Frame-Options on embedded routes.
  • third_party_prospect_origin — the single origin that postMessage events are sent to. If this is not set, no events are emitted at all.

These are configured independently, which produces a distinctive failure: set only the first and the iframe renders perfectly but your listener never fires. Send your Partner Success Manager both values, for staging and production. They are usually the same origin.

Give each one 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 it calls window.parent.postMessage(event, targetOrigin).

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 third_party_prospect_origin is set for the property owner; events are silently skipped when it is blank
  • Confirm the configured origin matches yours exactly — a trailing slash or a differing subdomain is enough to stop delivery
  • 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