Skip to content
Docs

02API guides

API v1For developers

Build your own platform integration

Rollmark has plugins for BigCommerce, WooCommerce and Adobe Commerce / Magento. For anything else (Shopify, a custom cart, an ERP or a POS) you build the integration yourself, with the same API the plugins use. This is the whole shape of one, for a store system that sells guns shipped through an FFL.

What you need

A store on Rollmark whose plan includes what you use, and one scoped private key with orders:read, orders:write and webhooks:manage (the "Order plugin" preset in Settings, API keys). The key lives on your server, never in a web page: put your own endpoint in front of your checkout page.

The flow

shopper picks an FFL          your server                         Rollmark
        │                          │                                 │
        │── ZIP ───────────────────▶ GET /checkout/ffls?zip=…  ─────▶│  (the picker; an FFL Checkout key)
        │◀─ FFLs ───────────────────│◀────────────────────────────────│
        │── places the order ───────▶ your system makes its order     │
        │                          │── POST /orders ─────────────────▶│  order made (open, matched)
        │                          │◀── 201 { data: { id, … } } ──────│
        │                          │                                 │  staff fill it, ship it
        │                          │◀── webhook order.shipped ───────│  signed
        │                          │── GET /orders/{id} ─────────────▶│  detail (tracking, lines)
        │◀─ "your order shipped" ───│  you update your order          │
  1. At checkout, choose the FFL. Use the FFL endpoints (GET /checkout/ffls, /checkout/ffls/{id}) with an FFL Checkout key, from your server, to power the picker. Keep the FFL's id with the order.
  2. When your own order exists, send it. POST /orders with the intake contract: your intake_ref (your cart or order id), your store_order_number (so the order is open and matched at once), the lines, the FFL and the shopper. Send an Idempotency-Key too. Keep the returned id. If you only know the cart when the shopper pays and the order number later, send POST /orders first (it stays pending) and call POST /orders/ref:<intake_ref>/match with { "store_order_number": "…" } when your order exists.
  3. Register one webhook, once, for order.shipped, order.cancelled, order.delivered and distributor_order.status_changed. Store the secret.
  4. When a webhook arrives: verify the signature, drop the event if you have its id, answer 200, then GET /orders/{order_id} and update your own order (shipment and tracking, or cancel).
  5. When you ship from your own admin (you, not Rollmark, ship): POST /orders/{id}/ship with { "tracking": "…" } and Idempotency-Key: ship-<your shipment id>. Marking an order shipped counts as a Rollmark Transfers shipment, the same as in the dealer app, and a gun does not ship to a dealer whose license copy is not on file; the API answers 409 invalid_state and says so.
  6. Catch up. Every 15 minutes: GET /orders?updated_since=<the time your last catch-up started>&limit=100, follow next_cursor, apply what changed. A webhook you missed then costs nothing.
  7. Errors. Retry 429 and 5xx with the same Idempotency-Key. Never retry a 4xx unchanged. Log request_id.

A reference outline in Node

This is the integration above in about sixty lines. It is a sketch to copy from, not a library: your system's calls are the ones named yourSystem.

Node
const BASE = 'https://app.getrollmark.com/api/v1';

async function rollmark(method, path, body, idempotencyKey) {
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.ROLLMARK_KEY}`,
      'Content-Type': 'application/json',
      ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) {
    const e = new Error(json.error?.message ?? `HTTP ${res.status}`);
    e.code = json.error?.code;
    e.status = res.status;
    e.requestId = json.error?.request_id;
    throw e;
  }
  return json;
}

// 1. Your checkout has made its own order: send it to Rollmark.
export async function sendOrder(order) {
  const { data } = await rollmark(
    'POST',
    '/orders',
    {
      intake_ref: `yourstore-${order.id}`,
      store_order_number: String(order.number),
      shopper: { name: order.customerName, email: order.email, phone: order.phone },
      billing: order.billingAddress,
      shipping: order.shippingAddress,
      ffl: { id: order.fflId },
      lines: order.items.map((i) => ({ sku: i.sku, upc: i.upc, title: i.title, quantity: i.qty, unit_price_cents: i.priceCents })),
    },
    `order-${order.id}`
  );
  return data.id; // keep it with your order
}

// 2. Register the webhook once (at install). Keep the secret.
export async function registerWebhook(url) {
  const { data } = await rollmark('POST', '/webhooks', {
    url,
    events: ['order.shipped', 'order.cancelled', 'order.delivered'],
  });
  return data.secret;
}

// 3. Your endpoint, after it has verified the signature (see Webhooks).
const seen = new Set(); // in production: a table, kept a few days
export async function onWebhook(event, yourSystem) {
  if (seen.has(event.id)) return;
  seen.add(event.id);
  const { data: order } = await rollmark('GET', `/orders/${event.data.order_id}`);
  if (event.type === 'order.shipped') {
    await yourSystem.markShipped(order.store_order_number, order.tracking);
  } else if (event.type === 'order.cancelled') {
    await yourSystem.cancel(order.store_order_number);
  }
}

// 4. You shipped it yourself: tell Rollmark.
export async function markShipped(rollmarkOrderId, shipmentId, tracking) {
  await rollmark('POST', `/orders/${rollmarkOrderId}/ship`, { tracking }, `ship-${shipmentId}`);
}

What the API never does

The API is for a store's own systems, so some things stay with the store's staff in Rollmark, for every plan and every key:

  • Sending a distributor order (or marking one ready, confirming, cancelling or receiving it). That spends the store's money and commits it to a distributor. The API can make drafts from order lines, which cost nothing; a person reviews and sends them.
  • Anything about a firearm's acquisition or disposition, and FastBound items: FastBound stays the record.
  • The store's cost and pricing rules, billing, staff and keys.
  • Hand-entered transfers (the Transfers tab), which pick a specific gun from FastBound.

Test it without a store's real orders

Make an order for a product and an FFL you control, watch it appear in the dealer app's Orders, then cancel it with POST /orders/{id}/cancel. Use Send test event on your webhook to check your endpoint without waiting for a real order.

Rollmark is made by Illumina Studios LLC. It was built at Ironcrest Arms LLC. Sample dealers, license numbers and costs in these docs are made up. Questions: [email protected].