Skip to main content

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:

HeaderValue
algES256
kidThe key id shown on the Embedding screen

The claims Nira reads are:

ClaimRequiredMeaning
assetyesThe 22-character id from the asset's /a/ URL
levelyesview or inspection
expyesExpiry time in seconds since the epoch. At most 4 hours from now
subnoYour id for the visitor, up to 200 characters
namenoVisitor name, up to 200 characters
emailnoVisitor 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.

ReasonMeaningWhat to do
expiredThe signature is valid and exp has passedSign a new token and load the new URL
invalidThe token itself is wrong: format, algorithm, kid, signature, claims, or an exp more than 4 hours aheadFix the signing code. A new token with the same mistake fails the same way
forbiddenThe 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 levelCheck the organization settings and the asset
rate_limitedToo many page loads with the same URL or for your organization, or too many renewal checksReduce 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":

ModeEffect
AnywhereAny website can embed your asset pages. This is the default.
Only on specific sitesOnly the sites you add can embed your asset pages.
NowhereNo 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.com allows every subdomain of example.com, but not example.com itself. 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, like http://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.