Integration patterns
Server-side calling, retries, idempotency, bulk imports, and conversion tracking patterns.
Prerequisites
Before building an integration with Nimriz, ensure you have:
- An active Nimriz workspace with API access.
- A workspace API key generated from IntegrationsDeveloperBackend API access in the dashboard.
- A conversion signing secret if you are implementing conversion tracking.
- A secure server-side environment to store credentials and make API calls.
Architecture: always call from your server
The core rule of Nimriz integration is simple: all API calls must come from your backend, never from browsers or client-side code.
Recommended server-side architecture:
Your user (browser/app)
↓
Your backend (trusted runtime)
↓
Nimriz API (api.nimriz.com)
- Your backend authenticates or authorizes the user in your own system.
- Your backend calls Nimriz with the workspace API key stored in a server environment variable.
- Your backend stores the
url_idandshort_urlreturned by Nimriz in your own database. - Your backend returns the relevant data to your user.
This pattern keeps your API key server-side and prevents quota abuse from browsers.
Use stable identifiers
Always store the url_id (UUID) as your primary reference to a Nimriz link in your own database. Do not rely solely on the short_code (slug) or short_url.
Reasons:
- Slugs can be changed via
PUT /api/update-slug. If you key your system by slug, a slug update breaks your reference. - The
url_idis permanent and never changes for the lifetime of a link. - The
domain_idis similarly permanent for your domain record.
// Good: store the durable identifier
await db.links.create({
nimrizUrlId: response.url_id, // permanent
shortUrl: response.short_url, // display/share value; may change if slug changes
shortCode: response.short_code,
});
// Risky: keying by slug only
await db.links.create({ slug: response.short_code }); // breaks if slug is updated
Retry rules
Retry these:
| Status | Reason | Strategy |
|---|---|---|
429 | Rate limited | Exponential backoff with jitter. |
502 | Bad gateway (transient) | Exponential backoff. |
503 | Service unavailable | Exponential backoff. |
504 | Gateway timeout | Exponential backoff. |
| Network timeout | Upstream connection lost | Backoff, then retry. |
Do not retry these:
| Status | Reason |
|---|---|
4xx (except 429) | Input errors, policy failures, auth failures. Retrying without fixing the input will fail again. |
slug_reserved, destination_blocked | Configuration issue-fix the input first. |
Exponential backoff implementation
async function callWithBackoff(
run: () => Promise<Response>,
maxAttempts = 4,
baseDelayMs = 500,
): Promise<Response> {
const retryableStatuses = new Set([429, 502, 503, 504]);
let delayMs = baseDelayMs;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await run();
if (response.ok || !retryableStatuses.has(response.status)) {
return response;
}
if (attempt === maxAttempts - 1) return response;
// Add jitter to prevent thundering herd
const jitter = Math.random() * 200;
await new Promise((resolve) => setTimeout(resolve, delayMs + jitter));
delayMs *= 2;
}
throw new Error('unreachable');
}
Idempotency for bulk imports
For any job-style link creation (CSV imports, batch automation, data migrations), always use POST /api/shorten/bulk with stable idempotency_key values.
Why idempotency keys matter
Network failures, timeouts, and server errors can leave you unsure whether a request completed. Without idempotency keys, retrying creates duplicate links. With a stable idempotency key, retrying returns the original result safely.
How to construct a good idempotency key
Build the key from stable fields that identify the exact intended outcome:
function buildIdempotencyKey(params: {
importSessionId: string;
rowId: string;
domainId: string;
longUrl: string;
customSlug?: string;
}): string {
const components = [
params.importSessionId,
params.rowId,
params.domainId,
params.longUrl,
params.customSlug ?? '',
].join(':');
return crypto.createHash('sha256').update(components).digest('hex').slice(0, 64);
}
If any intended input changes, generate a new key. Replaying an idempotency key with the same row payload returns the originally stored result. Replaying that key with a different row payload is rejected with idempotency_conflict, so changed inputs cannot silently return the old outcome.
Bulk import workflow
const results = await fetch('https://api.nimriz.com/api/shorten/bulk', {
method: 'POST',
headers: {
'Authorization': `Bearer ${WORKSPACE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
domain_id: DOMAIN_ID,
items: rows.map((row) => ({
client_row_id: row.id,
idempotency_key: buildIdempotencyKey({ importSessionId, rowId: row.id, ...row }),
long_url: row.destinationUrl,
custom_slug: row.slug,
})),
}),
}).then((r) => r.json());
// Process results per row, some may succeed while others fail validation
for (const result of results.results) {
if (result.ok) {
await saveLink(result.client_row_id, result.url_id, result.short_url);
} else {
await logFailedRow(result.client_row_id, result.code, result.error);
}
}
Key practices:
- Keep batches at 25 items or fewer.
- Store per-row outcomes in your own job log.
- Process valid rows even when other rows fail validation.
- On retry, replay only the rows that failed or were interrupted, using the same idempotency keys.
Password-protected links
If your integration creates or manages password-protected links:
- Send passwords only from your backend-never from the browser.
- Do not log plaintext passwords in your request logs or error tracking.
- Use
PUT /api/update-passwordwith"password": nullto remove protection. - Treat password updates as sensitive mutations-do not include them in generic retry loops.
Conversion tracking integration pattern
Conversion tracking uses signed server-to-server Conversion API requests. The browser never sends conversion data directly.
Recommended flow:
Click short link
↓
Nimriz appends nim_ct to the destination URL
↓
Your landing page captures nim_ct
↓
Your backend stores nim_ct on the lead/order/session
↓
Business event occurs (signup, payment, etc.)
↓
Your backend sends signed POST to Nimriz Conversion API
Capture nim_ct on the landing page
// Server-side (Next.js App Router, Express, etc.)
export async function GET(request: Request) {
const url = new URL(request.url);
const clickId = url.searchParams.get('nim_ct');
if (clickId) {
// Store on session, lead record, cart, or order metadata
await saveClickIdForSession(sessionId, clickId);
}
// ... render the page
}
Store nim_ct throughout the conversion funnel
| Funnel stage | Where to store nim_ct |
|---|---|
| Lead form | On the pending lead or CRM record. |
| E-commerce checkout | On the cart or order metadata before payment. |
| SaaS signup | On the pending account or trial record. |
| Billing lifecycle | Propagate with the order ID through your billing system for refund/cancellation events. |
Send the conversion event
import crypto from 'node:crypto';
async function sendConversionEvent(params: {
workspaceId: string;
secret: string;
eventName: 'lead' | 'sale' | 'refund' | 'cancellation' | 'reversal';
clickId?: string;
orderId?: string;
value?: number;
currency?: string;
externalId?: string;
idempotencyKey: string;
}) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
event_name: params.eventName,
event_time: new Date().toISOString(),
event_id: params.idempotencyKey,
user_data: {
click_id: params.clickId,
external_id: params.externalId,
},
custom_data: {
order_id: params.orderId,
value: params.value,
currency: params.currency,
},
});
const signature = `v1=${crypto
.createHmac('sha256', params.secret)
.update(`${timestamp}.${body}`)
.digest('hex')}`;
await fetch(
`https://api.nimriz.com/api/conversions/callback/${params.workspaceId}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Nim-Timestamp': timestamp,
'X-Nim-Signature': signature,
'Idempotency-Key': params.idempotencyKey,
},
body,
},
);
}
Troubleshooting
401 Unauthorized on API calls
- Confirm the
Authorization: Bearer <key>header format. - Confirm the key is active in your Integrations settings.
- Confirm the key belongs to the correct workspace.
slug_reserved errors in bulk imports
- The slug is either a reserved system path, below the domain's minimum slug length (usually 5 characters), or recently deleted.
- Generate slugs that are at least 5 characters long and avoid system paths.
Conversion events not appearing in the dashboard
- Confirm
nim_ctis being captured from the landing page URL and stored correctly through the funnel. - Confirm you are signing with the Conversion API signing secret, not the workspace API key.
- Use the same
Idempotency-Keyvalue on genuine retries of the same business event.
Bulk import rows partially failing
- Check the
okandcodefields per row in the response. Rows can fail for different reasons (slug taken, destination blocked, quota exceeded, etc.). - Fix the failing rows' inputs and replay those rows with new idempotency keys when the intended payload changed.
Related guides
Related next steps
Ready to test this setup?
Create an account to try the workflow, or compare plans when the setup needs higher limits, integrations, or team controls.