Partner Prospect API
The Prospect API lets you push renters to Rhino directly. You send what you know, Rhino validates it synchronously and checks eligibility in the background, and once the prospect is eligible it exposes an enrollment URL you send the renter to.
Rhino's preferred ingestion path is Partner Webhook Events, where you emit your existing events and write no client at all. Reach for the API when you want to go live without waiting on Rhino to build an adapter for your schema, when you want data problems reported back as a 422 at the moment you send them, or when you want to read prospect state on demand. Reading is useful on any integration — the GET endpoint works even if you write through events.
The write endpoint is an upsert keyed on (source, source_prospect_id) — the same key events use, so the two paths address the same records and can be mixed.
See Authentication & Environments for base URLs and request signing.
Create or update a prospect
POST /partners/{owner_slug}/prospects
Content-Type: application/json
{
"prospect": {
"source": "acme",
"source_prospect_id": "your-prospect-123",
"email": "renter@example.com",
"phone": "5559876543",
"first_name": "Jordan",
"last_name": "Rivera",
"birthdate": "1990-04-12",
"lease_start_date": "2026-09-01",
"lease_end_date": "2027-08-31",
"monthly_rent_cents": 200000,
"deposit_amount_cents": 200000,
"guarantor_coverage": false,
"screening_result": "approved",
"employment_status": "employed",
"yearly_income": 95000,
"has_ssn": true,
"social_security_number": "123-45-6789",
"pms_type": "yardi",
"pms_property_id": "prop-1",
"pms_unit_id": "unit-101",
"pms_prospect_id": "p0001234",
"terms_accepted_at": "2026-07-24T15:04:05Z"
}
}
Both a create and an update return the full prospect object with an HTTP status of 200. The status_code field inside the body is 201 when the prospect was created and 200 when an existing one was updated, so read that rather than the HTTP status if you need to tell them apart.
Only fields you include are updated. Omitting a field leaves the stored value untouched, so you can send partial updates as data becomes available.
Fetch a prospect
GET /partners/{owner_slug}/prospects/{source}/{source_prospect_id}
Returns the same prospect object. Use this to poll for eligibility if you are not subscribed to prospect.ready webhooks. After enrollment, offered_products is useful for security deposit insurance and cash deposit; for Renter Guarantee, read latest_coverage after purchase — offered_products stays empty.
Request fields
Three fields are enforced at write time. A request missing any of them is rejected with 422:
| Field | Notes |
|---|---|
source | Your partner code, assigned by Rhino. Must match a configured source. |
source_prospect_id | Your identifier for the renter. Unique per property owner and source. |
terms_accepted_at | ISO 8601 timestamp of when the renter accepted Rhino's terms in your flow |
Everything else is accepted as null, but the prospect stays incomplete and cannot enroll until the fields listed in Prospect Data Requirements are present.
All monetary values are integer cents (200000 is $2,000.00). Dates are YYYY-MM-DD; timestamps are ISO 8601.
Selecting Renter Guarantee
guarantor_coverage is an optional boolean and defaults to false. Send true when the renter should be offered Renter Guarantee instead of a deposit product. Rhino then evaluates Renter Guarantee eligibility for the property and marks the prospect ineligible for both security deposit insurance and cash deposit — the products are mutually exclusive. Once eligibility is written, that exclusivity shows up as a reason on those two products rather than as an error on your request.
Nothing else about Renter Guarantee is writable. Rhino resolves the guarantor application rule and the Renter Guarantee product version from the property's configuration; do not send an identifier or a version for either. deposit_amount_cents is not required for a guarantor_coverage: true prospect to leave incomplete — it's never the Renter Guarantee coverage amount, so omit it if the renter isn't giving a cash deposit.
Identifying the building and unit
A prospect must resolve to a building and a unit before it can enroll. How you identify them depends on how the property owner is configured:
- Owners synced from a PMS identify the building and unit with the
pms_type,pms_property_id, andpms_unit_idfields shown in the request above. Rhino matches them against the resident data it syncs from the PMS. - Owners you provision through the Partner Onboarding API identify them with nested
propertyandunitobjects, keyed on your ownexternal_system_id, the same identifiers you loaded through the properties and units endpoints. On these owners thepms_*fields are not used; send the objects instead.
{
"prospect": {
"source": "acme",
"source_prospect_id": "your-prospect-123",
"//": "renter and lease fields as above",
"property": { "external_system_id": "acme-prop-123" },
"unit": { "external_system_id": "acme-unit-456" }
}
}
Each object resolves in one of three ways:
- An identifier Rhino recognizes. The prospect attaches to that building or unit as a reference. Any other fields in the object are ignored, because roster changes go through the properties and units endpoints, never through a prospect.
- An unrecognized identifier with a full item. The building or unit is created, following the property or unit rules. If creation fails, the request returns
422with that item'serrorsand the prospect is not created. propertyabsent. The prospect is stored but cannot enroll, and no enrollment URL is returned.
An enrollment URL requires both the property and the unit to resolve. A prospect with only a property reference is stored but cannot enroll until its unit exists.
For an owner provisioned through the Onboarding API, deposit_amount_cents on the prospect is the renter's coverage amount, and property level coverage rules apply only where no deposit amount is supplied.
Response
{
"status_code": 201,
"prospect": {
"id": 4821,
"source": "acme",
"source_prospect_id": "your-prospect-123",
"email": "renter@example.com",
"phone": "5559876543",
"first_name": "Jordan",
"last_name": "Rivera",
"birthdate": "1990-04-12",
"lease_start_date": "2026-09-01",
"lease_end_date": "2027-08-31",
"screening_result": "approved",
"monthly_rent_cents": 200000,
"deposit_amount_cents": 200000,
"effective_coverage_amount_cents": 200000,
"guarantor_coverage": false,
"employment_status": "employed",
"yearly_income": 95000,
"has_ssn": true,
"pms_type": "yardi",
"pms_property_id": "prop-1",
"pms_unit_id": "unit-101",
"pms_prospect_id": "p0001234",
"status": "ready",
"percent_savings": 93.75,
"enrollment_url": "https://www.sayrhino.com/enrollment/embedded/acme-properties/acme/your-prospect-123?email=renter%40example.com",
"hosted_enrollment_url": "https://www.sayrhino.com/enrollment/expedited/acme-properties/acme/your-prospect-123?email=renter%40example.com",
"offered_products": [
{
"product_type": "security_deposit_insurance",
"upfront_premium_cents": 12500,
"monthly_premium_cents": 1400,
"upfront_enrollment_url": "https://www.sayrhino.com/enrollment/embedded/...&selected_product=security_deposit_insurance&cadence=upfront",
"monthly_enrollment_url": "https://www.sayrhino.com/enrollment/embedded/...&selected_product=security_deposit_insurance&cadence=monthly"
},
{
"product_type": "cash_deposit",
"cash_deposit_amount_cents": 200000,
"enrollment_url": "https://www.sayrhino.com/enrollment/embedded/...&selected_product=cash_deposit"
}
],
"application_submitted": false,
"latest_coverage": null
}
}
Every successful response is wrapped with a status_code key mirroring the HTTP status.
Note that id here is Rhino's numeric database ID. Webhook payloads use a prefixed string ID (tpp_…) for the same record. The stable identifier across both is your own source_prospect_id.
social_security_number, education_level, citizenship, and terms_accepted_at are accepted on write but are not echoed back in the response.
Renter Guarantee response
A prospect sent with guarantor_coverage: true carries its own eligibility fields:
{
"guarantor_coverage": true,
"rg_eligibility_status": "eligible",
"rg_ineligibility_reasons": [],
"status": "ready",
"enrollment_url": "https://www.sayrhino.com/enrollment/embedded/acme-properties/acme/your-prospect-123?email=renter%40example.com",
"offered_products": [],
"sdi_ineligibility_reasons": ["Renter Guarantee (guarantor coverage) selected — not eligible for SDA or cash deposit"],
"cd_ineligibility_reasons": ["Renter Guarantee (guarantor coverage) selected — not eligible for SDA or cash deposit"]
}
Three things differ from the deposit-product response. rg_eligibility_status is omitted while eligibility has not been decided — the prospect is incomplete, or processing after a write cleared the previous decision — so read a missing key as "not decided yet", not as ineligible. rg_ineligibility_reasons is always present while guarantor_coverage is true, as [] when there is nothing to report. And offered_products never contains a priced Renter Guarantee row: it holds security deposit insurance and cash deposit only, so it stays empty for the whole Renter Guarantee lifecycle.
That means there is nothing to wait for beyond enrollment_url and hosted_enrollment_url. Once they are populated, send the renter to one of them; pricing is presented inside the enrollment flow.
The Renter Guarantee coverage amount comes from the property's guarantor application rule, or the property or unit coverage settings, and may be a rent multiplier or a fixed amount — not from deposit_amount_cents or effective_coverage_amount_cents. There is no top-level Renter Guarantee coverage field; after purchase it appears as coverage_amount_cents on latest_coverage.
Response fields
| Field | Description |
|---|---|
status | incomplete, processing, ready, or error |
effective_coverage_amount_cents | The deposit figure Rhino's deposit products are written against, derived from the property owner's rules rather than your deposit_amount_cents. Not the Renter Guarantee coverage amount. |
percent_savings | Savings on a security deposit insurance premium versus the deposit it replaces, as a percentage. Empty until the renter has been underwritten in the enrollment flow, and always empty on a Renter Guarantee prospect. |
enrollment_url | Link to start enrollment, on the flow your source is configured for. null unless the prospect is eligible for at least one product. |
hosted_enrollment_url | The same enrollment, always on the expedited (full-page) flow, for a renter you cannot iframe — a native app, or a link in an email or SMS. Same nil semantics as enrollment_url, and identical to it when your source is already expedited. Load it top-level: it is not frameable. |
offered_products | Priced deposit products with prefilled enrollment links, on your source's configured flow — there is no hosted variant. Empty until the renter has been underwritten in the enrollment flow, and always empty on a Renter Guarantee prospect. |
application_submitted | Whether the renter has submitted an application |
latest_coverage | The renter's existing policy or cash deposit for this unit, if any. type is security_deposit_insurance, renter_guarantee_insurance, jetty_deposit, or cash_deposit. |
guarantor_coverage | Whether the prospect is on Renter Guarantee. Always present. |
rg_eligibility_status | eligible or ineligible. Present only when guarantor_coverage is true, and omitted then too until eligibility has been decided. |
rg_ineligibility_reasons | Human-readable reasons Renter Guarantee was declined. Present whenever guarantor_coverage is true, as an empty array when there are none; omitted entirely when it is false. |
sdi_ineligibility_reasons | Human-readable reasons SDA was declined. Omitted when there are none. After eligibility is written on a Renter Guarantee prospect, this carries the product-exclusivity reason. |
cd_ineligibility_reasons | Human-readable reasons cash deposit was declined. Omitted when there are none. After eligibility is written on a Renter Guarantee prospect, this carries the product-exclusivity reason. |
Product objects
offered_products contains up to two entries, both deposit products:
security_deposit_insurance — upfront_premium_cents, monthly_premium_cents, upfront_enrollment_url, monthly_enrollment_url
cash_deposit — cash_deposit_amount_cents, enrollment_url
There is no renter_guarantee_insurance entry. Renter Guarantee is priced inside the enrollment flow, so do not gate sending a Renter Guarantee renter on a product row appearing here.
Prospect lifecycle
incomplete ──(all required fields present)──> processing ──(eligibility)──> ready
▲ │
└──────────────(a required field is cleared)──────────────────────────────┘
incomplete— required fields are missing. No enrollment URL.processing— Rhino has what it needs and is checking eligibility. This is asynchronous and typically takes seconds.ready— Rhino has finished processing. The enrollment URLs are populated if the prospect is eligible for at least one product that applies to it; send the renter to one of them.error— eligibility could not be determined. Contact your Partner Success Manager.
A prospect that is ready but ineligible for a given product carries the reasons in sdi_ineligibility_reasons, cd_ineligibility_reasons, or rg_ineligibility_reasons. When every applicable product is declined, both enrollment URLs are null, so gate on the URL rather than on status.
Read those reasons per product rather than in aggregate. After eligibility is written, a Renter Guarantee prospect is by design ineligible for security deposit insurance and cash deposit, and carries the exclusivity reason on both, while still having populated enrollment URLs when Renter Guarantee itself is eligible.
If enrollment_url is null on a Renter Guarantee prospect that is ready with rg_eligibility_status of ineligible, rg_ineligibility_reasons explains why. During incomplete or processing, a null URL with no rg_eligibility_status and an empty rg_ineligibility_reasons array means eligibility has not been decided yet.
Because processing resolves asynchronously, subscribe to the prospect.ready webhook rather than polling in a tight loop.
ready does not mean the prospect is priced. Underwriting needs a few data points Rhino collects from the renter during enrollment, so offered_products and percent_savings stay empty until the renter has been through the flow — and on a Renter Guarantee prospect they never populate. Do not build a UI that quotes Rhino before then.
Errors
Validation failures return 422 with per-field messages keyed by attribute name:
{
"status_code": 422,
"error": "Prospect invalid.",
"prospect": {
"source_prospect_id": "Source prospect has already been taken",
"terms_accepted_at": "Terms accepted at can't be blank"
}
}
| Status | Meaning |
|---|---|
401 | Request signature missing, malformed, or older than five minutes |
404 | Unknown owner_slug, or the prospect does not exist on GET |
422 | Validation failure — see the prospect object for field-level detail |
Test data reset
In staging only, you can clear test records so you can re-run an enrollment with the same identifiers:
DELETE /residents/reset?email={email}&owner_slug={owner_slug}
Both parameters are required, and both go in the query string rather than the body. This clears the renter's Rhino user along with the prospects and policies attached to it, and returns every resident matching the email under that owner to a pre-enrollment state — unlinking the policy, removing the tenant record, and re-evaluating eligibility so an earlier decline does not carry over. The same email can then go through enrollment again. It returns 404 in production.
The call is idempotent: an email with nothing left to clear returns 200 with empty id lists rather than an error, so it is safe to call unconditionally at the start of every test run. What it cannot do is create a resident — if the email has no resident under the owner, there is nothing to enroll and you will need one set up for you.
Note the path: unlike the rest of the API, this one is not scoped under /partners/{owner_slug}.
Next steps
- Prospect Data Requirements — which fields drive completeness
- Embedded purchase flow — using the returned enrollment URL
- Webhooks Overview — reacting to status changes