# Developer Integration API Reference

> Connect a custom storefront to Refermate: the capture script, the server postback API for orders, refunds, and subscriptions, response codes, and how to verify tracking.

- Canonical URL: https://partners.refermate.com/developers
- Base URL: https://partners.refermate.com

## How it works

1. A creator shares a Refermate link. Refermate records the click and sends the shopper to your site with an `rm_click` query parameter.
2. The capture script stores that value in a first-party cookie on your site.
3. At checkout you save the click ID with the order.
4. When the order is paid, your server posts it to Refermate. Money moves only on these server postbacks, never on browser events.
5. You approve conversions in Partners. Refermate bills your saved payment method after the refund hold and pays the creator.

## Capture script

Add this to the `<head>` of every page a creator link can land on.

```html
<script src="https://partners.refermate.com/api/pn/sdk/v1.js" data-rm-consent-mode="explicit" data-rm-max-age-days="30" defer></script>
```

- Without a consent attribute, the script assumes your consent manager loaded it after consent. With `data-rm-consent-mode="explicit"`, it stores and reads nothing until you call `window.RefermateCapture.grantConsent()`, and that holds on every page: call it on each page load once the shopper has accepted, including checkout, or `getClick()` returns null. The click is captured only on the landing page, so it is lost if consent arrives after the shopper leaves it.
- A consent manager can decide before the deferred script loads. Keep the decision and call `grantConsent()` once `window.RefermateCapture` exists, for example from the script tag's `load` event.
- `window.RefermateCapture.getClick()` returns the stored click ID for your checkout.
- The cookie is `_refermate_click`, host-only and `SameSite=Lax`. `data-rm-max-age-days` asks for 1 to 365 days (default 30), but browsers can expire it sooner: Safari keeps cookies set by scripts for at most 7 days. The last click wins.
- If your router rewrites the URL before deferred scripts run, drop `defer`.

## Authentication

Send your secret postback key as `Authorization: Bearer <key>` (or `X-Refermate-Postback-Key`). Bodies are JSON, up to 16 KiB. Call these endpoints only from your server; the key must never reach a browser.

## Endpoints

### POST /api/partner-network/postbacks/<public-tracking-key>/conversions

Report a paid one-time order that carries a Refermate click. Post only orders with a click; an order without one is rejected and shows as a tracking error.

| Field | Required | Description |
| --- | --- | --- |
| `order_id` | Yes | Your stable order ID, up to 500 characters. It is the idempotency key: resending the same order is safe. |
| `click_id` | Yes | The `rm_click` value the capture script stored for this shopper. An order without one is rejected with 422 `missing_referral_click`. |
| `currency` | Yes | Three-letter currency code. Only `USD` is accepted. |
| `order_subtotal_cents` | Yes | Order subtotal in cents, a whole number of 0 or more. |
| `occurred_at` | Yes | When it happened, in ISO 8601 with an explicit offset and 0 to 3 fractional digits, for example `2026-09-28T17:05:00.000Z`. JavaScript's `toISOString()` qualifies; truncate microseconds from other languages. |
| `eligible_amount_cents` | No | The commissionable part of the subtotal. Defaults to the subtotal and cannot exceed it. |
| `line_items` | No | `[{ product_id, amount_minor_units, quantity }]`, summing to the eligible amount. Required when the program pays per product. |
| `order_name` | No | A customer-facing order number. |
| `customer_id` | No | Your customer ID, for your own reporting. |
| `discount_code` | No | A discount code used on the order. |
| `metadata` | No | An object of your own data, up to 4 KiB. |
| `test` | No | `true` only for the verification order from your setup page. |

```json
{
  "click_id": "a1B2c3D4e5F6g7H8",
  "currency": "USD",
  "eligible_amount_cents": 10000,
  "occurred_at": "2026-09-28T17:05:00.000Z",
  "order_id": "ORDER-1001",
  "order_name": "#1001",
  "order_subtotal_cents": 10000
}
```

### POST /api/partner-network/postbacks/<public-tracking-key>/reversals

Report a full refund or cancellation of a one-time order. Partial refunds are not accepted yet. A refund that arrives after Refermate has billed you for the commission returns 422 `paid_conversion_not_auto_reversed` and reverses nothing; contact support to settle it.

| Field | Required | Description |
| --- | --- | --- |
| `order_id` | Yes | The same order ID you sent to `conversions`. |
| `occurred_at` | No | When it happened, in ISO 8601. Defaults to when we receive the request; send it when you replay a late event. |
| `reason` | No | A short reason such as `refunded` or `cancelled`. |

```json
{
  "occurred_at": "2026-10-02T09:30:00.000Z",
  "order_id": "ORDER-1001",
  "reason": "refunded"
}
```

### POST /api/partner-network/postbacks/<public-tracking-key>/v2/recurring/relationships

Bind a new subscription to the Refermate click that started it. Send this once, when the subscription is created. Add a subscription commission rule for the product in Partners first: a bind with no matching rule returns `no_matching_subscription_rule` and keeps that subscription at zero commission, even after you add a rule.

| Field | Required | Description |
| --- | --- | --- |
| `event_id` | Yes | Your unique ID for this event; resending it is safe. |
| `relationship_id` | Yes | Your stable subscription ID. |
| `click_id` | Yes | The `rm_click` value for this shopper. |
| `product_id` | Yes | The subscribed product. |
| `price_id` | No | The subscribed price or plan. |
| `currency` | Yes | Only `USD` is accepted. |
| `environment` | Yes | Always `live` for production traffic. |
| `occurred_at` | No | When it happened, in ISO 8601. Defaults to when we receive the request; send it when you replay a late event. |

```json
{
  "click_id": "a1B2c3D4e5F6g7H8",
  "currency": "USD",
  "environment": "live",
  "event_id": "evt-sub-1001-created",
  "occurred_at": "2026-09-28T17:05:00.000Z",
  "product_id": "plan-pro",
  "relationship_id": "sub-1001"
}
```

### POST /api/partner-network/postbacks/<public-tracking-key>/v2/recurring/billing-events

Report each invoice of a bound subscription, paid or not. Never send a click here: the relationship already names it.

| Field | Required | Description |
| --- | --- | --- |
| `event_id` | Yes | Your unique ID for this event. |
| `relationship_id` | Yes | The subscription ID you bound. |
| `invoice_id` | Yes | Your invoice ID. |
| `payment_state` | Yes | `paid`, `failed`, `open`, `void`, or `uncollectible`. Only `paid` can earn a reward. |
| `amount_paid_minor_units` | Yes | Amount paid, in cents. |
| `eligible_amount_minor_units` | No | The commissionable part. Defaults to the amount paid and cannot exceed it. |
| `product_id` | Yes | The billed product. A product or price different from the bound one holds the subscription for review. |
| `price_id` | No | The billed price or plan. |
| `currency` | Yes | Only `USD` is accepted. |
| `environment` | Yes | Always `live` for production traffic. |
| `occurred_at` | Yes | When it happened, in ISO 8601 with an explicit offset and 0 to 3 fractional digits, for example `2026-09-28T17:05:00.000Z`. JavaScript's `toISOString()` qualifies; truncate microseconds from other languages. |

```json
{
  "amount_paid_minor_units": 1999,
  "currency": "USD",
  "environment": "live",
  "event_id": "evt-inv-1001-paid",
  "invoice_id": "inv-1001",
  "occurred_at": "2026-09-28T17:05:00.000Z",
  "payment_state": "paid",
  "product_id": "plan-pro",
  "relationship_id": "sub-1001"
}
```

### POST /api/partner-network/postbacks/<public-tracking-key>/v2/recurring/reversals

Report a full refund of one subscription invoice.

| Field | Required | Description |
| --- | --- | --- |
| `event_id` | Yes | Your unique ID for this event. |
| `relationship_id` | Yes | The subscription ID. |
| `invoice_id` | Yes | The refunded invoice. |
| `reason` | No | A short reason. |
| `environment` | Yes | Always `live` for production traffic. |
| `occurred_at` | No | When it happened, in ISO 8601. Defaults to when we receive the request; send it when you replay a late event. |

```json
{
  "environment": "live",
  "event_id": "evt-inv-1001-refunded",
  "invoice_id": "inv-1001",
  "occurred_at": "2026-10-02T09:30:00.000Z",
  "reason": "refunded",
  "relationship_id": "sub-1001"
}
```

### POST /api/partner-network/postbacks/<public-tracking-key>/v2/recurring/relationships/end

Report that a subscription ended. Later invoices for it are rejected.

| Field | Required | Description |
| --- | --- | --- |
| `event_id` | Yes | Your unique ID for this event. |
| `relationship_id` | Yes | The subscription ID. |
| `reason` | No | A short reason. |
| `environment` | Yes | Always `live` for production traffic. |
| `occurred_at` | No | When it happened, in ISO 8601. Defaults to when we receive the request; send it when you replay a late event. |

```json
{
  "environment": "live",
  "event_id": "evt-sub-1001-ended",
  "occurred_at": "2026-12-28T17:05:00.000Z",
  "reason": "canceled",
  "relationship_id": "sub-1001"
}
```

## Responses

Every response is JSON with `ok` and a machine-readable `reason`, for example `{"ok": true, "reason": "recorded", "transaction_id": 123}`. Created records add `transaction_id`, `conversion_id`, `binding_id`, or `event_id`. Branch on the status and `reason`, never on wording.

| Status | Meaning |
| --- | --- |
| 200 | Accepted, including an exact replay of an earlier request (for example `duplicate_order`). |
| 400 | The body is not valid JSON or is missing a required field (`invalid_payload`, `json_required`, `unexpected_click_id`). Fix the request; do not retry it. |
| 401 | Missing or wrong postback key. |
| 403 | The integration is disabled or the store is archived. |
| 404 | The subscription, invoice, or click named in a recurring request does not exist. |
| 409 | The same ID was sent earlier with different content (`duplicate_order_conflict`, `duplicate_event_conflict`), or the environment does not match. |
| 410 | A verification test link expired. |
| 413 | The body is larger than 16 KiB. |
| 422 | The request is valid but cannot earn, for example `missing_referral_click`, `referral_click_already_converted`, `unsupported_pricing_currency`, `invalid_occurred_at`, or `relationship_ended`. Do not retry. One needs you: `paid_conversion_not_auto_reversed` means a refund came after the commission was billed, so contact support. |
| 5xx | Usually temporary: retry the same body with backoff. `503` means pricing history is briefly unavailable. A `500` with reason `duplicate_event` repeats a recurring event that was already rejected; it will never succeed, so stop retrying it. A `500` with reason `active_hold` means the order is under a dispute or review hold; the refund succeeds only once that resolves, so retry it daily, not with fast backoff. |

## Retries and limits

- Retry only 5xx responses and network errors, with the same body. Never retry a `500` whose reason is `duplicate_event`, and retry `active_hold` daily.
- One click backs one one-time order. A second order on the same click returns `referral_click_already_converted`.
- Only USD orders earn today.
- `occurred_at` may be at most 5 minutes in the future. A paid order may be at most the program's attribution window plus 7 days old; refunds and subscription events may be up to 372 days old.

## Verify your integration

In Partners, open your Developer Integration setup page and choose "Create test link". Open the link on your site, place an order, and post it with `"test": true` and the `rmv_` click ID the script stored. A 200 `test_verified` response turns tracking on. Test orders never create money.
