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)
  1. Your backend authenticates or authorizes the user in your own system.
  2. Your backend calls Nimriz with the workspace API key stored in a server environment variable.
  3. Your backend stores the url_id and short_url returned by Nimriz in your own database.
  4. 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_id is permanent and never changes for the lifetime of a link.
  • The domain_id is 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:

StatusReasonStrategy
429Rate limitedExponential backoff with jitter.
502Bad gateway (transient)Exponential backoff.
503Service unavailableExponential backoff.
504Gateway timeoutExponential backoff.
Network timeoutUpstream connection lostBackoff, then retry.

Do not retry these:

StatusReason
4xx (except 429)Input errors, policy failures, auth failures. Retrying without fixing the input will fail again.
slug_reserved, destination_blockedConfiguration 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-password with "password": null to 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 stageWhere to store nim_ct
Lead formOn the pending lead or CRM record.
E-commerce checkoutOn the cart or order metadata before payment.
SaaS signupOn the pending account or trial record.
Billing lifecyclePropagate 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_ct is 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-Key value on genuine retries of the same business event.

Bulk import rows partially failing

  • Check the ok and code fields 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.