Show reports in your platform
Your users read a Carousel report inside your own product, with its PDF. They don’t need a Carousel login, and your system has nothing to store but ids.
For platforms whose users don’t sign in to Carousel — a leasing tool, a CRM, a property-management app. If your users already have Carousel logins, show them portalUrl instead: see Send and follow screenings.
A sample CRM that sends, tracks, then shows Carousel’s real report inside it — click through exactly what your users get, then the code behind it: the full example, in Integrate your CRM.
What your user sees
How it works
One Carousel account is enough for a whole platform: its key sends every screening and makes every link, and your own login decides which of your users sees which application. Carousel doesn’t keep your customers apart — your server does: a key that shows reports can make a link for any report the key reaches, so check which customer an application belongs to before each link — or give each customer a key limited to their own workspace, which reaches no one else’s reports. Integrate your CRM says what else they share.
Set it up once
Make a key that can show reports
Account menu → Developers → Keys, with Show reports in my app on: a test key (cp_test_…) to build, a live key for real reports. Keep it on your server. Choose which workspaces it reaches as you make it: every workspace you can use, or only the ones you tick — a key per customer, limited to that customer’s workspace, shows no one else’s reports. Its holder can open any report the key reaches, documents included, so guard it like a results key; it just can’t call the results API. A key with Read applicant results can make links too. If you’re a developer on someone else’s account, a live key with this option needs the owner to switch on Can see applicant data for you (Developers → Access); test keys don’t. Switching it off later doesn’t stop keys already made: revoke those under Keys.
Allow your platform’s addresses
Account menu → Developers → Allowed sites: add each exact address your app runs on — https://app.your-platform.com, every subdomain or custom domain your customers use, and http://localhost:3000 (your port) while you build — localhost shows test reports only; a live one needs your own https site. Paste a page’s address and its site is kept. No wildcards; up to 25 — if your customers use more domains, show reports from pages on one domain of yours, like reports.your-platform.com. A site works from the next view; until it’s added, the frame there says “This report can’t be shown on this site”.
Keep Carousel’s screening id with your record
One per applicant. Send your own id as externalId on every send: GET /screenings?externalId=<your id> returns each applicant you sent with it — see Send and follow screenings.
Ask for a link when your user opens the report
From your server, once you’ve checked that this user may see this application:
curl -X POST https://app.oncarousel.com/api/v1/screenings/<id>/view-links \
-H "Authorization: Bearer $CAROUSEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "viewer": "jane@agency.example" }'201:
{
"url": "https://app.oncarousel.com/embed/r/kV9q2xR7mPz4tLwB8nYc3hJd6sFg1aEu5oKiQvXbN0e",
"expiresAt": "2026-10-06T15:02:00.000Z"
}viewer is your user, as you name them — an email or your own id, up to 200 characters. Carousel records it with the link and every view; it doesn’t check it. A key can make 120 links a minute, apart from its 120 requests.
Put it in a frame straight away
<iframe src="https://app.oncarousel.com/embed/r/kV9q2xR7…"
title="Screening report"
style="width:100%; height:calc(100vh - 64px); border:0"></iframe>The same thing in a Node server, from your user’s click to the page:
app.get('/applications/:id/report', requireLogin, async (req, res) => {
const record = await db.applications.find(req.params.id);
// Your rule: only the user's own applications.
if (!record || record.agencyId !== req.user.agencyId) return res.sendStatus(404);
const answer = await fetch(
`https://app.oncarousel.com/api/v1/screenings/${record.carouselScreeningId}/view-links`,
{
method: 'POST',
headers: { Authorization: `Bearer ${process.env.CAROUSEL_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ viewer: req.user.email }),
},
);
if (!answer.ok) {
const { error } = await answer.json(); // { code, message, hint }
return res.status(502).render('report-unavailable', { code: error.code });
}
const { url } = await answer.json();
res.set('Cache-Control', 'no-store'); // the link works once: never serve this page from a cache
res.render('report', { frameUrl: url }); // <iframe src="{{frameUrl}}">
});Your page
- Allow the frame in your own CSP. If your page sends a
Content-Security-Policy, addframe-src https://app.oncarousel.com. - Don’t sandbox the iframe. The report runs scripts and downloads its PDF; a
sandboxattribute breaks both. - Load each link once. The first load spends it, so a frame that mounts twice — a re-render that resets
src, React’s StrictMode in development — shows “This view has ended”. Ask for a new link instead of reusing one. - Give it room. The report scrolls inside the frame. It shows two columns from 900 px wide and stacks below that; a height of the window less your header works well.
- Native apps: open an HTTPS page of yours, on your allowed sites, that holds the frame — not the link itself.
One link, two clocks
- The link works once, within 2 minutes: long enough for your page to load the frame, and useless after.
- The view it opens lasts 30 minutes. Switching tabs, opening documents and downloading the PDF all happen inside it.
- When it ends, the frame shows “This view has ended” and tells your page, so you can offer a fresh one:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://app.oncarousel.com') return;
if (event.data?.type === 'carousel.view_ended') showReopenButton(); // asks your server for a new link
});The message comes when an open view’s 30 minutes run out. A link that was already used, or is over 2 minutes old, shows the same words without it — so keep a way to reopen on your page as well.
Build it with a test key
A test key’s link opens Carousel’s sample report — a fictional applicant under a Test mode bar — through exactly the same steps: one use, 2 minutes, 30 minutes, the PDF, the ending. Send a test screening, ask for its link, and frame it from localhost once that address is in your allowed sites. No real applicant’s data is read, nothing is charged or logged, and no screening.viewed is sent.
What’s in the frame
| Shown | Not shown |
|---|---|
| The Overview and every check’s results | Your team’s notes and who it’s assigned to |
| The stored documents: ID, court files, uploads | Reminders, cancelling, adding people |
| The PDF: preview, print view and download — without the “Also available on Carousel” list of checks this screening didn’t include | The other applicants on the file — each has their own link |
| Where the rental is and its rent | Your workspaces, costs and settings |
Nothing in the frame leads into Carousel. Before the applicant finishes, the report says what’s still waiting (“Nothing back yet — 3 checks waiting on the applicant”). A Québec court file that hasn’t been opened shows its number only.
Who viewed it
Each time the report loads in the frame — the first load, a reload, the print view, every PDF download — your webhook gets a screening.viewed, saying whose it was, once the applicant has opened their link. One view can send several:
{
"type": "screening.viewed",
"data": {
"screening": { "id": "6f0c…", "externalId": "APP-88231", "portalUrl": "…" },
"via": "embed",
"viewer": "jane@agency.example"
}
}via is embed for a view through a link, carousel for your team in Carousel. See Events and webhooks.
How it stays safe
| Rule | What it means |
|---|---|
| One use, 2 minutes | A copied or forwarded link opens nothing. So does one opened by an email’s link scanner. |
| One report | A view opens the screening it was made for, one the key reaches, and nothing else. |
| Only in your platform | The report shows only inside the sites your account allowed — the key’s account’s, even for a report in a workspace another account shared with you — and never as a page of its own — not in a new tab, not from an email. |
| Read-only | Nothing in the frame can change the screening. |
| Logged | Every link is recorded with your viewer, and so is every view, document and PDF once the applicant has opened their link — before that, the report has nothing to read. |
| Revocable | Revoking the key stops all its links and views at once: nothing more loads, and no document or PDF opens. So does the key’s owner losing the screening’s workspace, when a share ends for example: the view checks the key’s reach every time it opens something. A report already on screen stays there until the frame reloads or its 30 minutes end. |
| Nothing to store | The results stay with Carousel; your system keeps one id per applicant. |
When something goes wrong
Asking for a link:
| HTTP | Code | What to do |
|---|---|---|
| 400 | invalid_request | Send viewer, 1 to 200 characters, and nothing else. |
| 401 | unauthorized | Send Authorization: Bearer <key>. |
| 403 | view_links_not_allowed | Make the key with Show reports in my app on. |
| 404 | not_found | Use the id of a screening this key reaches (a test id with a test key), once its invitation went out. |
| 429 | rate_limited | Over 120 links, or 120 requests of any kind, a minute for this key. Wait the seconds in Retry-After. |
| 503 | busy | Carousel is briefly at capacity: retry after Retry-After. |
In the frame:
| Your user sees | Why | What to do |
|---|---|---|
| This view has ended | Its 30 minutes are up, the link was used already or is over 2 minutes old, or the key was revoked. | Make a new link. |
| This report opens inside your platform | The link or view was opened as a page of its own. | Open it in your frame. |
| Too many requests | One view loaded over 60 times in 10 minutes. (Over 300 documents or 6 PDFs in 10 minutes, the file just doesn’t open.) | Make a new link, or wait for the 10 minutes to pass. |
| “This report can’t be shown on this site”, or the browser’s “refused to connect” | Your page’s address isn’t in your allowed sites — or it’s localhost and the key is live. | Add it in Developers → Allowed sites; it works from the next view. Then make a new link. |
Questions
Do my users need a Carousel account?
No. Your platform signs them in; Carousel trusts your server to ask only for reports they may see.
Can I try it before going live?
Yes: with a test key, the link opens Carousel’s sample report. Add http://localhost:<port> to your allowed sites to frame it from your machine.
Can I email the link, or make it ahead of time?
No. It works once and only inside your platform. Make one each time a user opens a report.
What if my user opens the report in a new tab?
The new tab shows “This report opens inside your platform”. The frame keeps working.
Can they download the report?
Yes. The PDF tab in the frame previews it and downloads it to their device. It doesn’t pass through your server.
An application has two applicants. How do I show both?
Each applicant is their own screening, so each has their own link and frame. Send each one with your record’s id as externalId, and ?externalId= finds them all. A co-applicant someone adds in Carousel has externalId null, so it isn’t found that way.
Each of my customers has their own domain. Do you need all of them?
Yes — each exact address the frame shows on, added in Developers → Allowed sites (up to 25). Add new ones as your customers arrive; they work from the next view. Wildcards aren’t accepted: past 25, show reports from pages on one domain of yours.
Show a report only to the people in your platform who need it to assess that application. Carousel records who you named for every view. See Carousel’s Privacy Policy and Terms. The Tenant Screening Acceptable Use Policy applies to your own systems and connected assistants too.

