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)
  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 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-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 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 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 only those rows with the same idempotency keys.

Related guides