# API

A RESTful API for managing your affiliate program: associates, referral links, clicks, and conversions.

## Organisations and Programs

An **Organisation** is your login — the email you use to sign in to the dashboard, via a passwordless emailed sign-in link.

A **Program** is a single affiliate program's configuration: its destination URL, commission rules, currency, cookie/payout windows, and its own API key. 

One Organisation can run multiple Programs (e.g. one per product or site), and each Program's API key only ever sees that Program's own associates, links, clicks, and conversions — never another Program's, even under the same Organisation.

Signing up creates one Organisation and its first Program together. 

Add more Programs from the dashboard at any time, each with its own independent API credentials.

## Authentication

Endpoints under `/api/*` are authenticated with **HTTP Basic Auth**, using a program's API key as the username and API secret as the password:

```
Authorization: Basic base64(API_KEY:API_SECRET)
```

With curl, pass `-u API_KEY:API_SECRET` and it will encode this for you. A program's API key and secret are shown once when it's created (at signup, or when adding a new program from the dashboard) — or regenerated from that program's Credentials page.

Requests without valid credentials return `401`:

```json
{ "error": "Invalid or missing credentials" }
```

## Sandbox environment

`https://sandbox.affiliately.io` is a separate deployment of the exact same application — identical behavior to production, but backed by its own database. Point an integration at it while you're building or testing, without touching real associates, links, clicks, or conversions.

- Every endpoint in this document works the same way on both hosts — swap `affiliately.io` for `sandbox.affiliately.io` in the URL and nothing else changes.
- Sandbox credentials are separate from production. A production API key/secret will not work against the sandbox host, and vice versa.
- Sandbox is the right place to exercise flows that create real tracking data as a side effect — e.g. repeatedly hitting [`GET /r/:code`](#test-a-referral-link) — since none of it is real.

## Manage a program

Requires auth, using the program's own API key. Lets a program manage its own configuration — not the organisation's login, which has no API-key-driven equivalent and is managed from the dashboard only.

An API key/secret only ever identifies one program, so you don't need to already know your program's `id` — `GET /api/programs/me` looks it up for you:

```shell
curl -u API_KEY:API_SECRET https://affiliately.io/api/programs/me
```

Once you have the `id` from that (or from the dashboard), the same record is also reachable at `/api/programs/:id` — useful if you've cached it and don't want the extra lookup — and that's the form `PATCH`/`DELETE` use:

```shell
curl -u API_KEY:API_SECRET https://affiliately.io/api/programs/6

curl -u API_KEY:API_SECRET \
  -X PATCH https://affiliately.io/api/programs/6 \
  -H "Content-Type: application/json" \
  -d '{ "commissionRules": { "billing": "one-off", "calculation": "flat", "amount": 120 } }'

curl -u API_KEY:API_SECRET -X DELETE https://affiliately.io/api/programs/6
```

`PATCH` accepts any of `name`, `destinationUrl`, `commissionRules`, `currency`, `cookieDurationDays`, `payoutHoldDays`, `attributionModel`. `DELETE` is a soft delete, like everything else in this API, and only ever affects the program identified in the URL — never the organisation or its other programs.

`attributionModel` is `first-touch` or `last-touch` (default). It decides which associate gets credit when a customer clicks more than one referral link before completing an order:

- **`last-touch`** — the most recent click wins.
- **`first-touch`** — the original click wins, even if the customer clicked other links afterwards.

> **This is a declared intent, not something Affiliately enforces.** Since `/r/:code` doesn't set a cookie (see [Test a referral link](#test-a-referral-link)), Affiliately never sees which `afly` value the customer's browser is actually carrying — only whichever one your backend sends to `POST /api/conversions`. Implementing `attributionModel` means writing your own cookie-setting logic to match: for `last-touch`, always overwrite your cookie with the newest `afly`; for `first-touch`, only set it if one isn't already there. This field exists so that's a documented, discoverable setting instead of tribal knowledge scattered across your codebase.

### Commission rules

`commissionRules` is a single JSON object describing how much an associate earns, and when. Every conversion (and every renewal — see [Subscriptions](#subscriptions)) reads this to work out its payout, so it's worth understanding the shape up front. It's a discriminated object — which fields are required or forbidden depends on `billing` and `calculation`:

| Field        | Type    | Required                              | Notes                                                                 |
|--------------|---------|----------------------------------------|------------------------------------------------------------------------|
| `billing`    | string  | yes                                     | `one-off` or `subscription`                                            |
| `recurrence` | string  | only if `billing` is `subscription`     | `once` (commission on the first payment only) or `every-cycle` (commission on renewals too). Forbidden when `billing` is `one-off` |
| `cycleLimit` | integer | no, and only when `recurrence` is `every-cycle` | Caps commission to the first N renewals; omit or send `null` for uncapped. Forbidden otherwise |
| `calculation`| string  | yes                                     | `flat` or `percentage`                                                 |
| `amount`     | number  | only if `calculation` is `flat`         | The flat commission amount, in the program's `currency`. Forbidden when `calculation` is `percentage` |
| `pct`        | number  | only if `calculation` is `percentage`   | 0–100. Applied to `orderValue` (see [Conversions](#conversions)). Forbidden when `calculation` is `flat` |

Three example shapes:

Flat one-off — a fixed fee per sale:
```json
{ "billing": "one-off", "calculation": "flat", "amount": 25 }
```

Percentage one-off — 10% of the order value, one-time:
```json
{ "billing": "one-off", "calculation": "percentage", "pct": 10 }
```

Subscription — paid every renewal, capped at 12 cycles:
```json
{ "billing": "subscription", "recurrence": "every-cycle", "cycleLimit": 12, "calculation": "flat", "amount": 15 }
```

Sending a field that doesn't apply to your combination (e.g. `pct` alongside `calculation: "flat"`, or `recurrence` alongside `billing: "one-off"`) is rejected with `400`.

## Create an associate

Registers a new associate (affiliate) under your program.

```shell
curl -u API_KEY:API_SECRET \
  -X POST https://affiliately.io/api/associates \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Doe",
    "email": "jane@example.com",
    "metadata": { "externalId": "usr_9f2a", "tier": "gold" }
  }'
```

| Field     | Type   | Required | Notes                                    |
|-----------|--------|----------|-------------------------------------------|
| `name`    | string | yes      |                                            |
| `email`   | string | yes      |                                            |
| `status`  | string | no       | `pending` (default) or `approved` — see below |
| `metadata`| object | no       | Arbitrary JSON for your own use — stored and returned as-is, never read or acted on by the API |

> **Security note:** `metadata` is stored and returned in plaintext — there's no encryption at rest and no masking in responses. If you're storing sensitive values here (bank details, government IDs, etc.), encrypt them yourself before sending and decrypt them on your side after fetching. Affiliately never reads, validates, or acts on `metadata` contents, so anything you don't protect yourself is kept exactly as sent.

An associate's `status` is one of:

- **`pending`** — awaiting your review. The default if omitted, so associates are held for approval unless you opt in to `approved`.
- **`approved`** — pre-approved and live immediately.
- **`rejected`** — reviewed and declined; never went live.
- **`suspended`** — was live, now pulled from the program (e.g. for policy violations, or on their own request).

`rejected` and `suspended` cannot be set on creation — only reachable via [update](#update-an-associate). Every status is reversible via update (e.g. `rejected` → `approved` on reconsideration, `suspended` → `approved` to reinstate).

Response — `201 Created`:

```json
{
  "id": 16,
  "programId": 23,
  "name": "Jane Doe",
  "email": "jane@example.com",
  "status": "pending",
  "metadata": { "externalId": "usr_9f2a", "tier": "gold" },
  "createdAt": "2026-07-07T20:07:16.754Z",
  "updatedAt": "2026-07-07T20:07:16.754Z"
}
```

**List / get associates** — requires auth, list filters by `email`:

```shell
curl -u API_KEY:API_SECRET "https://affiliately.io/api/associates?email=jane@example.com"
```

Useful for looking up a specific associate by email without fetching every associate and filtering client-side.

## Update an associate

Requires auth. Approve or reject a pending associate, suspend one, or change any other field:

```shell
curl -u API_KEY:API_SECRET \
  -X PATCH https://affiliately.io/api/associates/16 \
  -H "Content-Type: application/json" \
  -d '{ "status": "approved" }'
```

Accepts any of `name`, `email`, `status` (`pending`, `approved`, `rejected`, or `suspended`), `metadata`.

## Create a link

Generates a trackable referral link for one of your associates. `code` is what shows up in the actual URL a customer clicks (`https://affiliately.io/r/CODE`) — omit it to get a random one, or supply your own vanity code.

```shell
curl -u API_KEY:API_SECRET \
  -X POST https://affiliately.io/api/links \
  -H "Content-Type: application/json" \
  -d '{
    "associateId": 17,
    "code": "SUMMER10",
    "discountPct": 20
  }'
```

| Field             | Type    | Required | Notes                                                              |
|-------------------|---------|----------|---------------------------------------------------------------------|
| `associateId`     | integer | yes      | Must belong to your program                                         |
| `code`            | string  | no       | Alphanumeric, dashes, and underscores, 3–32 chars. Auto-generated if omitted. Must be unique |
| `discountPct`     | integer | no       | 0–100, default `0`. Share of the flat commission passed on as a customer discount instead of associate payout |
| `destinationUrl`  | string  | no       | Overrides the program's default destination URL for this link      |

Response — `201 Created`:

```json
{
  "id": 16,
  "associateId": 17,
  "programId": 24,
  "code": "SUMMER10",
  "discountPct": 20,
  "createdAt": "2026-07-07T20:10:41.800Z",
  "updatedAt": "2026-07-07T20:10:41.800Z"
}
```

If `associateId` doesn't belong to your program (or doesn't exist), you get `400`:

```json
{ "error": "associateId does not reference an existing associate" }
```

## Test a referral link

`GET /r/:code` is the public, unauthenticated endpoint a customer's browser actually hits when they click a referral link. It logs a click and redirects to the destination URL, appending the click's token as an `afly` query param. You can inspect this with curl.

Affiliately doesn't set a cookie of its own — the `afly` query param on the redirect is the only signal it hands you. **It's on you to persist that value** as the customer moves from the landing page to checkout (e.g. your own first-party cookie, session, or localStorage) so it's still available when you call [checkout](#apply-the-discount-at-checkout) or [report a conversion](#conversions).

By default curl does **not** follow redirects, so `-i` (include headers) is enough to see the raw `302` response — the `Location` it would have sent a browser to:

```shell
curl -i https://affiliately.io/r/SUMMER10
```

```
HTTP/1.1 302 Found
Location: https://your-destination.example.com/?afly=7403952b-4b3e-40e5-aac0-11084b5cfcf1
...
```

To capture just the redirect target (e.g. for scripting a test), use `-w`:

```shell
curl -o /dev/null -w "status: %{http_code}\nredirected to: %{redirect_url}\n" \
  https://affiliately.io/r/SUMMER10
```

Two things worth knowing:

- **`-L` follows the redirect** instead of just showing it — useful to confirm the destination page actually loads, but combine it with `-i` if you still want to see the 302 and its headers along the way.
- **This isn't idempotent.** Every request logs a new `Click` and mints a fresh `afly`, exactly like a real visitor clicking the link — so repeated test runs create real tracking data, not just a preview.

## Apply the discount at checkout

Once `afly` is on the cart/checkout page (carried over from the redirect's `?afly=` query param — it's on you to persist it that far, see above), get a quote for the discount to apply *before* the order is placed. This is a read, not a report — it doesn't touch `Click`/`Conversion` data, so you can call it as many times as the cart re-renders.

```shell
curl -u API_KEY:API_SECRET "https://affiliately.io/api/checkout?aflyToken=7403952b-4b3e-40e5-aac0-11084b5cfcf1"
```

| Query param  | Type   | Required                                  | Notes                                                                 |
|--------------|--------|--------------------------------------------|--------------------------------------------------------------------------|
| `aflyToken`  | string | yes                                        | The `afly` value from the referral redirect                              |
| `orderValue` | number | only if your program's `calculation` is `percentage` | The order's total value, before any discount — the base the percentage is applied to |

```shell
curl -u API_KEY:API_SECRET "https://affiliately.io/api/checkout?aflyToken=7403952b-4b3e-40e5-aac0-11084b5cfcf1&orderValue=200"
```

Response — `200 OK`. The commission is first resolved from your program's `commissionRules` — either the flat `amount`, or `pct`% of `orderValue` — then split by the link's `discountPct`. This is the same two-step calculation `POST /api/conversions` repeats once the order is placed (see [Conversions](#conversions)). You apply `customerDiscount` to the cart; `associatePayout` is informational, for display if you want it; `currency` is your program's `currency` (e.g. `GBP`/`USD`/`EUR`) — `commissionAmount`/`customerDiscount`/`associatePayout` mean nothing without it:

```json
{
  "linkId": 16,
  "associateId": 17,
  "code": "SUMMER10",
  "discountPct": 20,
  "commissionAmount": 100,
  "currency": "GBP",
  "customerDiscount": 20,
  "associatePayout": 80
}
```

`404` if `aflyToken` doesn't match a click for your program, or if the click's attribution window (`cookieDurationDays`) has already expired — treat either case as "no discount applies":

```json
{ "error": "No click found for that aflyToken" }
```

`400` if your program's `calculation` is `percentage` and `orderValue` was omitted:

```json
{ "error": "orderValue is required to quote a percentage-based commission" }
```

## Clicks

`GET /r/:code` (above) is how a real visitor's browser generates a click. The `/api/clicks` endpoints are for managing that data directly — `POST` is a raw, unauthenticated way to log a click without an actual browser redirect (e.g. from a mobile app or server-side integration); `GET`/`DELETE` are authenticated and scoped to your own program's links.

**Create a click** — public, no auth required:

```shell
curl -X POST https://affiliately.io/api/clicks \
  -H "Content-Type: application/json" \
  -d '{ "linkId": 17 }'
```

| Field         | Type    | Required | Notes                                          |
|---------------|---------|----------|--------------------------------------------------|
| `linkId`      | integer | yes      |                                                    |
| `aflyToken` | string  | no       | UUID. Auto-generated if omitted                   |
| `ipAddress`   | string  | no       | Defaults to the requester's IP                    |
| `userAgent`   | string  | no       | Defaults to the request's `User-Agent` header     |

Response — `201 Created`:

```json
{
  "id": 42,
  "linkId": 17,
  "aflyToken": "600aaeb9-6115-4fbc-93d2-3ec216ec4a53",
  "ipAddress": "203.0.113.10",
  "userAgent": "curl/8.7.1",
  "createdAt": "2026-07-07T20:19:50.935Z"
}
```

**List clicks** — requires auth, filter by `linkId` and/or `aflyToken`:

```shell
curl -u API_KEY:API_SECRET "https://affiliately.io/api/clicks?linkId=17"
```

```json
[
  {
    "id": 42,
    "linkId": 17,
    "aflyToken": "600aaeb9-6115-4fbc-93d2-3ec216ec4a53",
    "ipAddress": "203.0.113.10",
    "userAgent": "curl/8.7.1",
    "createdAt": "2026-07-07T20:19:50.000Z",
    "deletedAt": null
  }
]
```

**Get one click** — requires auth:

```shell
curl -u API_KEY:API_SECRET https://affiliately.io/api/clicks/42
```

**Delete a click** — requires auth. Clicks use soft deletes, so this hides the row from the API rather than erasing it:

```shell
curl -u API_KEY:API_SECRET -X DELETE https://affiliately.io/api/clicks/42
```

Response — `204 No Content`.

## Conversions

Reported by your own backend when an order completes, using the `afly` value your frontend picked up from the redirect and persisted through to checkout (see [Test a referral link](#test-a-referral-link)). The API looks up the matching click, attributes it to the right associate/link, and computes the payout split — you never send amounts yourself.

**Report a conversion** — requires auth:

```shell
curl -u API_KEY:API_SECRET \
  -X POST https://affiliately.io/api/conversions \
  -H "Content-Type: application/json" \
  -d '{
    "aflyToken": "9d2a19ed-54d7-486d-ad19-7561eaff4e02",
    "orderReference": "ORDER-1042"
  }'
```

| Field            | Type   | Required | Notes                                             |
|------------------|--------|----------|-----------------------------------------------------|
| `aflyToken`    | string | yes      | The `afly` value from the referral redirect     |
| `orderReference` | string | yes      | Your own order/invoice id. Must be unique per program |
| `orderValue`     | number | only if your program's `calculation` is `percentage` | The order's total value, before any discount — the base the percentage is applied to |

Response — `201 Created`. `commissionAmount` is resolved from your program's `commissionRules` — either the flat `amount`, or `pct`% of `orderValue` — then `customerDiscount` and `associatePayout` are split from that according to the link's `discountPct`:

```json
{
  "id": 38,
  "programId": 26,
  "linkId": 18,
  "aflyToken": "9d2a19ed-54d7-486d-ad19-7561eaff4e02",
  "orderReference": "ORDER-1042",
  "commissionAmount": 100,
  "currency": "GBP",
  "associatePayout": 80,
  "customerDiscount": 20,
  "status": "pending",
  "createdAt": "2026-07-07T20:24:10.745Z",
  "updatedAt": "2026-07-07T20:24:10.745Z"
}
```

`currency` is captured from your program at the moment the conversion is reported and stays fixed from then on — same as `commissionAmount` — so a later change to your program's currency never relabels a historical payout.

If your program's `commissionRules.billing` is `subscription`, this call is also the signup moment: it creates a `Subscription` behind the scenes and stamps the conversion with `subscriptionId` and `cycleNumber: 1`. See [Subscriptions](#subscriptions) for how renewal payments are reported from here on.

Error responses:

| Status | Cause                                                                              |
|--------|--------------------------------------------------------------------------------------|
| `400`  | No click found for that `aflyToken`                                                |
| `400`  | Your program's `calculation` is `percentage` and `orderValue` was omitted             |
| `409`  | `orderReference` has already been reported for this program                          |
| `410`  | The click's attribution window has expired (past the program's `cookieDurationDays`) |

**List / get conversions** — requires auth, list filters by `linkId`, `status`, and/or `orderReference`:

```shell
curl -u API_KEY:API_SECRET "https://affiliately.io/api/conversions?status=pending"
curl -u API_KEY:API_SECRET "https://affiliately.io/api/conversions?orderReference=ORDER-1042"
curl -u API_KEY:API_SECRET https://affiliately.io/api/conversions/38
```

`orderReference` is how you go from your own order id to the conversion's internal `id` — useful before a `PATCH` (e.g. [canceling one during the hold period](#cancel-a-conversion-during-the-hold-period)), since that endpoint only accepts the `id`.

**Find conversions ready to be paid** — `readyForPayment=true` returns `pending`/`approved` conversions whose `payoutHoldDays` window has already elapsed (i.e. exactly the set you could successfully `PATCH` to `paid` right now, without doing that date math yourself):

```shell
curl -u API_KEY:API_SECRET "https://affiliately.io/api/conversions?readyForPayment=true"
```

This ignores `status` if both are passed together — `readyForPayment` already implies `pending` or `approved`.

**Update status** — requires auth. This is how you move a conversion through `pending` → `approved` → `paid`, or mark it `rejected`:

```shell
curl -u API_KEY:API_SECRET \
  -X PATCH https://affiliately.io/api/conversions/38 \
  -H "Content-Type: application/json" \
  -d '{ "status": "approved" }'
```

`approved` is purely informational — nothing in the API requires it. There's no enforced state machine beyond the two rules below, so a conversion can go straight from `pending` to `paid` once the hold period ends without ever passing through `approved`. In practice, you'd set `approved` whenever your own backend considers the sale verified (e.g. payment/fraud checks clear) — independent of the cancellation window — to distinguish "reviewed and legitimate, just waiting out the hold period" from "still unreviewed."

Two rules are enforced server-side, not left to the caller:

- **Payout hold period.** You can't mark a conversion `paid` until `payoutHoldDays` have passed since it was created — this is your program's cancellation/returns window:

  ```json
  { "error": "This conversion cannot be marked paid until its 14-day hold period ends (eligible at 2026-07-21T20:24:10.000Z)" }
  ```

- **Once paid, it's final.** Any further `PATCH` or `DELETE` on a `paid` conversion returns `409`:

  ```json
  { "error": "This conversion has already been paid and cannot be modified" }
  ```

**Delete a conversion** — requires auth, soft delete, blocked once `paid` (see above):

```shell
curl -u API_KEY:API_SECRET -X DELETE https://affiliately.io/api/conversions/38
```

### Cancel a conversion during the hold period

If a customer cancels before a conversion has been paid, look it up by your own order id, then mark it `rejected`:

```shell
curl -u API_KEY:API_SECRET "https://affiliately.io/api/conversions?orderReference=ORDER-1042"
```

```shell
curl -u API_KEY:API_SECRET \
  -X PATCH https://affiliately.io/api/conversions/38 \
  -H "Content-Type: application/json" \
  -d '{ "status": "rejected" }'
```

This works at any point before the conversion is `paid` — unlike `paid`, `rejected` isn't gated by `payoutHoldDays`, so it doesn't matter whether the cancellation happens on day 1 or day 13 of the window.

## Subscriptions

Only relevant if your program's `commissionRules.billing` is `subscription`. A **Subscription** tracks one associate's recurring customer across billing cycles, so renewal payments can be credited without a fresh referral click — which won't exist by the time a renewal invoice comes due; the attribution window has long since expired.

**The first payment still goes through `POST /api/conversions`**, exactly as documented in [Conversions](#conversions) — it's the signup moment, with a real `aflyToken` from the original click. For a subscription-billed program, that call also creates the `Subscription` for you and stamps the conversion with `subscriptionId` and `cycleNumber: 1`:

```json
{
  "id": 40,
  "programId": 26,
  "linkId": 18,
  "subscriptionId": 5,
  "cycleNumber": 1,
  "aflyToken": "9d2a19ed-54d7-486d-ad19-7561eaff4e02",
  "orderReference": "ORDER-1042",
  "commissionAmount": 15,
  "currency": "GBP",
  "associatePayout": 15,
  "customerDiscount": 0,
  "status": "pending",
  "createdAt": "2026-07-07T20:24:10.745Z",
  "updatedAt": "2026-07-07T20:24:10.745Z"
}
```

**Report a renewal** — requires auth. There's no `aflyToken` for a renewal — you identify the subscription by its own `id` instead:

```shell
curl -u API_KEY:API_SECRET \
  -X POST https://affiliately.io/api/subscriptions/5/renewals \
  -H "Content-Type: application/json" \
  -d '{ "orderReference": "ORDER-1042-CYCLE-2", "orderValue": 50 }'
```

| Field            | Type   | Required | Notes                                                    |
|------------------|--------|----------|-------------------------------------------------------------|
| `orderReference` | string | yes      | Your own order/invoice id for this cycle. Must be unique per program |
| `orderValue`     | number | only if your program's `calculation` is `percentage` | Same rule as `POST /api/conversions` |

Response — `201 Created`, same shape as a conversion, with `cycleNumber` incremented from whatever this subscription is currently at. Whether this particular cycle actually earns commission depends on `commissionRules`:

- **`recurrence: "once"`** — only cycle 1 (the original conversion) ever pays. Every renewal after that is still recorded here, for your own order history, but with `commissionAmount`, `customerDiscount`, and `associatePayout` all `0`.
- **`recurrence: "every-cycle"`, no `cycleLimit`** — every renewal pays, indefinitely.
- **`recurrence: "every-cycle"`, with a `cycleLimit`** — renewals pay until the cycle count passes the limit, then `0` from then on (same zeroed-out shape as above).

A zeroed-out renewal is still a normal `201` — it's not an error, it's simply a cycle your `commissionRules` say shouldn't pay.

Error responses:

| Status | Cause                                                                  |
|--------|---------------------------------------------------------------------------|
| `400`  | Your program's `calculation` is `percentage` and `orderValue` was omitted |
| `404`  | No subscription with that `id` for your program                          |
| `409`  | That `orderReference` has already been reported for this program          |
| `409`  | The subscription has been cancelled                                       |

**Cancel a subscription** — requires auth. Stops it from accepting further renewals:

```shell
curl -u API_KEY:API_SECRET -X POST https://affiliately.io/api/subscriptions/5/cancel
```

Response — `200 OK`, the updated `Subscription`. Any renewal reported against a cancelled subscription from then on returns `409`.

**List / get subscriptions** — requires auth, list filters by `linkId` and/or `status`:

```shell
curl -u API_KEY:API_SECRET "https://affiliately.io/api/subscriptions?status=active"
curl -u API_KEY:API_SECRET https://affiliately.io/api/subscriptions/5
```

```json
{
  "id": 5,
  "programId": 26,
  "linkId": 18,
  "aflyToken": "9d2a19ed-54d7-486d-ad19-7561eaff4e02",
  "status": "active",
  "createdAt": "2026-07-07T20:24:10.000Z",
  "updatedAt": "2026-07-07T20:24:10.000Z"
}
```
