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 Dashboard → Settings → Integrations → API access.
- 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 input changes, generate a new key. An idempotency key with a different payload is rejected.
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-use 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 a signed server-to-server callback pattern. 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 only those rows with the same idempotency keys.