Signed asset URLs
Signed asset URLs let your backend give a visitor short-lived access to one private asset. The visitor needs no Nira account, and the embed works without third-party cookies.
Your server signs a token with a private key that only you hold. Nira checks it with the matching public key. The token goes into the iframe URL.
Signed URLs require a Growth or Enterprise plan and the purchase of the Role-based Access Control for iframes add-on. To renew tokens while a visitor is viewing, you also need the Viewer API add-on. An organization admin can buy both under Admin Options > Billing.
Add a signed URL key
Open Admin Options > Embedding and add a key. You have two choices:
- Generate key makes a key pair in the browser. The private key is shown once. Copy or download it, because Nira does not keep it.
- Use your own key lets you paste a PEM public key. The key must be an ES256 key (curve P-256).
To make a P-256 key pair yourself with OpenSSL, run:
openssl ecparam -name prime256v1 -genkey -noout -out nira-signing-key.pem
openssl ec -in nira-signing-key.pem -pubout -out nira-signing-key.pub.pem
Paste the contents of nira-signing-key.pub.pem into Nira. nira-signing-key.pem is the private key your server signs with.
You can add up to 5 keys, each with an optional expiry date. Nira assigns each key a kid. Put it in the token header.
To rotate a key, add a new key, switch your server to sign with it, then delete the old key. A deleted key stops verifying tokens within a minute.
Keep the private key on your server. Never put it in a web page or an app.
Build the URL
Sign a JWT with the ES256 algorithm. The header needs two fields:
| Header | Value |
|---|---|
alg | ES256 |
kid | The key id shown on the Embedding screen |
The claims Nira reads are:
| Claim | Required | Meaning |
|---|---|---|
asset | yes | The 22-character id from the asset's /a/ URL |
level | yes | view or inspection |
exp | yes | Expiry time in seconds since the epoch. At most 4 hours from now |
sub | no | Your id for the visitor, up to 200 characters |
name | no | Visitor name, up to 200 characters |
email | no | Visitor email, up to 200 characters |
Other claims are ignored.
Put the token in the path of your Nira site URL:
https://your-org.nira.app/s/<token>
The same URL works with or without the Viewer API. Put the token in the path only. Query strings and hashes pass through unchanged.
Sign a token
The samples read the private key from nira-signing-key.pem. A key generated in the browser downloads as nira-signing-key-<kid>.pem, so rename the file or change the path in the sample.
Node, with jsonwebtoken:
import fs from "node:fs";
import jwt from "jsonwebtoken";
const privateKeyPem = fs.readFileSync("nira-signing-key.pem", "utf8");
const token = jwt.sign(
{
asset: "kS3dX9mQ2vYb7LpR4tWn1A",
level: "view",
exp: Math.floor(Date.now() / 1000) + 10 * 60,
sub: "visitor-1234",
},
privateKeyPem,
{ algorithm: "ES256", keyid: "YOUR_KID", noTimestamp: true }
);
const url = `https://your-org.nira.app/s/${token}`;
Python, with PyJWT[crypto]:
import time
import jwt
with open("nira-signing-key.pem") as f:
private_key_pem = f.read()
token = jwt.encode(
{
"asset": "kS3dX9mQ2vYb7LpR4tWn1A",
"level": "view",
"exp": int(time.time()) + 10 * 60,
"sub": "visitor-1234",
},
private_key_pem,
algorithm="ES256",
headers={"kid": "YOUR_KID"},
)
url = f"https://your-org.nira.app/s/{token}"
C#, with System.IdentityModel.Tokens.Jwt (.NET 5 or later, for ImportFromPem):
using System;
using System.IO;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
var privateKeyPem = File.ReadAllText("nira-signing-key.pem");
var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(privateKeyPem);
var key = new ECDsaSecurityKey(ecdsa) { KeyId = "YOUR_KID" };
var credentials = new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256);
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("asset", "kS3dX9mQ2vYb7LpR4tWn1A"),
new Claim("level", "view"),
new Claim("sub", "visitor-1234"),
}),
Expires = DateTime.UtcNow.AddMinutes(10),
SigningCredentials = credentials,
};
var token = new JwtSecurityTokenHandler().CreateEncodedJwt(descriptor);
var url = $"https://your-org.nira.app/s/{token}";
Access levels
view gives read-only access. The visitor can look at the asset and its callouts. Nothing the visitor does is saved, and reports are not available.
inspection gives the same access as an inspection link. The visitor can create, edit and delete callouts and photo markups, and can create and read reports.
A valid token opens the asset even if the asset has a password. The signed token takes the place of the password.
Token lifetime
Nira rejects a token whose exp is more than 4 hours ahead.
With the Viewer API, use short tokens of 5 to 15 minutes and renew them while the visitor is looking at the asset. Nira checks exp exactly, with no extra allowance, so keep the clock on your signing server accurate.
Without the Viewer API, the token cannot be renewed. Set exp to cover the visit you expect, up to the 4 hour cap.
Renew with the Viewer API
About 60 seconds before the token expires, the viewer emits access_renewal_needed with the current exp. Ask your backend for a new URL, then pass it to renewAccess:
viewer.on("access_renewal_needed", async ({ exp }) => {
const response = await fetch("/api/nira-url");
const { url } = await response.json();
const { exp: newExp } = await viewer.renewAccess(url);
});
The new URL must name the same asset and the same level as the current one, on the same Nira host. Otherwise renewAccess rejects with forbidden and the current token keeps working until it expires. The camera, the tool mode and the session continue without a reload.
renewAccess rejects with an Error whose message is the reason, or timeout if the viewer does not answer within 15 seconds.
The viewer can also ask for renewal when the idle overlay appears and when a hidden tab becomes visible again near expiry.
Expiry and errors
When access ends, the viewer emits access_expired with one of these reasons. The page shows an access message instead of the asset.
| Reason | Meaning | What to do |
|---|---|---|
expired | The signature is valid and exp has passed | Sign a new token and load the new URL |
invalid | The token itself is wrong: format, algorithm, kid, signature, claims, or an exp more than 4 hours ahead | Fix the signing code. A new token with the same mistake fails the same way |
forbidden | The token is valid but access is refused: the Role-based Access Control for iframes add-on is not enabled, public links are off, the asset is missing or archived, or a renewal named a different asset or level | Check the organization settings and the asset |
rate_limited | Too many page loads with the same URL or for your organization, or too many renewal checks | Reduce how often you load the same URL |
Re-sign only on expired. Signing again after the other reasons repeats the same refusal.
The viewer relays at most 3 access_expired events per 60 seconds per Viewer instance. It drops the rest.
Embedding notes
Lazy-loaded iframes: if you use loading="lazy", create the token when the iframe is about to load. A token minted at page load may expire before the visitor scrolls to the iframe.
Firefox restores the last URL of an iframe when the parent page reloads. That URL holds an old token. Set the src from script after the page loads, or give the iframe a unique name so Firefox does not match it to the earlier one.
Do not log or share URLs that contain a token. Anyone holding an unexpired token has the access it grants.
Allowed embedding sites
You can choose which websites can show your organization's asset pages in an iframe. Open Admin Options > Embedding and pick one of three modes under "Allowed embedding sites":
| Mode | Effect |
|---|---|
| Anywhere | Any website can embed your asset pages. This is the default. |
| Only on specific sites | Only the sites you add can embed your asset pages. |
| Nowhere | No website can embed your asset pages. |
This setting controls where the viewer can appear, not who can see an asset. Signed URLs, roles and passwords still decide access. It applies to every page on your Nira site, including asset pages, inspection links, signed URLs and the sign-in page.
Allowed embedding sites require a Growth or Enterprise plan and the purchase of the Role-based Access Control for iframes add-on. An organization admin can buy it under Admin Options > Billing. If it is removed later, Nira stops applying your list and keeps it.
Nira sends the setting in a Content-Security-Policy: frame-ancestors header. When a browser blocks an embed, the browser refuses to load the page in the frame and reports it in the developer console.
Site entries
- Enter a site name, like
example.com. - To embed your assets in your own callouts and descriptions, list your Nira site, like
yourorg.nira.app, and your custom domain if you have one. *.example.comallows every subdomain ofexample.com, but notexample.comitself. List both if you need both.- An entry without a scheme matches
https://pages only. For a local test page, add the scheme and port, likehttp://localhost:3000. - A pasted link, like
https://www.example.com/portal/page, is shortened to its site,https://www.example.com. - Spaces, quotes, commas and semicolons are refused.
- You can list up to 20 sites, with at most 2,048 characters in total.
Pages inside other apps
The browser checks every page around the viewer, not only the page that holds the iframe. If your page is itself shown inside another app, such as SharePoint, Teams, Notion, Google Sites or a Webflow preview, list that app's site too.
A callout in another Nira organization that embeds one of your assets is blocked unless that organization's Nira site is listed.