# Agilis REST API reference

> The Agilis advertising REST API lets advertisers manage offers programmatically. Human-readable version: https://www.agilis.dating/api-doc · OpenAPI 3.1: https://www.agilis.dating/openapi.json

- Base URL: https://ads.agilis.dating
- Authentication: header `x-api-key: YOUR_API_KEY_HERE` (never Bearer)
- Responses: JSON. Success is `return_code` `S1` (`S2` on resume); errors are `E1`–`E9`, **scoped to the endpoint**: the same code can mean different things on different endpoints.

## Authentication notes

- API keys are created in the Agilis Ad Portal under **Manage API Keys** (Developer role).
- **A key is shown once.** The Ad Portal shows the full key only when you create it (or roll it). Copy it then and store it securely: Agilis does not keep a copy, so it cannot be shown again. The key list shows only its last four characters. If a key is lost or may have been exposed, use **Roll key**: you get a new key, shown once, and the old one stops working immediately.
- **Each key works in one mode only, and this is enforced:** the mode the Ad Portal was in (its test/live mode toggle) when the key was created. A `pk_live_` key is a LIVE key and a `pk_test_` key is a TEST key. To work in both modes, create one key in each.
- A request whose `environment` names the other mode is refused with HTTP 403 before anything is created or charged. The message names your key's mode: `{"return_code": "E1", "status": "Error", "code": 403, "message": "This is a LIVE API key, so it can only be used with environment=LIVE. Use a TEST key (created in the Ad Portal in TEST MODE) for TEST requests."}`
- Offers of the other mode are treated as not found: pause, resume, analytics and delete return HTTP 401 (`E1`, "Offer not found" on delete), exactly as for an offer that does not exist.
- A key with no recorded mode returns HTTP 401 with `{"return_code": "E1", "message": "This API key has no environment. Create a new key in the Ad Portal."}`. Create a new key.
- A disabled key or disabled user account returns `HTTP 401 {"message": "Unauthorised"}` on any endpoint.
- Keys have an optional expiry date; expired keys also return 401.
- **Calls are recorded for 90 days.** Every request made with your API key, including refused ones, is recorded with the key, the endpoint (its route, such as `offers/{id}/pause`, without ids or the query string), the HTTP status, the `return_code`, the response time and the time of the call. The request body, the query string and your IP address are not recorded. Records are deleted after 90 days. Recording does not change any request or response.

## Rate limits

- **60 requests a minute** per client IP address, across every endpoint.
- Every response carries `X-RateLimit-Limit` (the requests allowed in the current minute) and `X-RateLimit-Remaining` (how many are left).
- Over the limit, the API returns **HTTP 429** with a `Retry-After` header: the number of seconds to wait before trying again. Retry a refused `POST /offers` with the same `Idempotency-Key`.

## Developer Guide

This guide walks through the complete offer creation flow, from fetching categories to managing a live offer. The Agilis Advertising API lets developers create and manage advertiser offers programmatically.

- **Base URL:** `https://ads.agilis.dating`
- **Authentication:** All requests require a valid API key in the `x-api-key` request header.
- **Content-Type:** `application/json` for JSON requests; `multipart/form-data` for offer creation.

### Overview

The offer creation flow involves up to five steps, depending on the payment method chosen:

```
1. Fetch Categories        GET  /categories
   Fetch Interests         GET  /interests                   (only if targeting by interest)
2. Estimate Price          POST /offers/price-estimate       (recommended before creating)
3. Create Offer            POST /offers
   ├─ Payment method E ──► Stripe checkout link returned
   │                        User completes payment externally
   │                        Webhook activates offer → Active or Scheduled
   └─ Payment method B ──► Ad Units deducted from balance immediately
                            Offer becomes Active or Scheduled at once
```

Once the offer exists you can view it, pause/resume it, retrieve analytics, or delete it.

### Step 1: Fetch Categories

Before creating an offer you need a valid `category_id`. Retrieve the categories assigned to your organisation.

**Endpoint:** `GET` `/categories`

```
curl -X GET "https://ads.agilis.dating/categories" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Successful response — HTTP 200

```
{
  "categories": [
    { "category_id": 1, "category_name": "Retail" },
    { "category_id": 2, "category_name": "Food & Beverage" },
    { "category_id": 3, "category_name": "Health & Fitness" }
  ]
}
```

Use the `category_id` value in Step 3.

#### Error responses

| HTTP | Body | Meaning |
|---|---|---|
| 401 | `{"message": "Unauthorised"}` | API key is missing, invalid, or disabled; or the associated user account is disabled |
| 500 | `{"message": "Internal server error"}` | Unexpected server error |

### Step 1b: Fetch Interests (Optional)

Offers can optionally be targeted at users who have selected particular interests. Retrieve the available interests to obtain their ids. Skip this step if you do not intend to target by interest.

**Endpoint:** `GET` `/interests`

```
curl -X GET "https://ads.agilis.dating/interests" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Successful response — HTTP 200

```
{
  "interests": [
    { "interest_id": 3, "interest_name": "Cooking" },
    { "interest_id": 7, "interest_name": "Fitness" },
    { "interest_id": 12, "interest_name": "Live music" }
  ]
}
```

Use the `interest_id` values in the optional `interests` array in Step 3.

#### Error responses

| HTTP | Body | Meaning |
|---|---|---|
| 401 | `{"message": "Unauthorised"}` | API key is missing, invalid, or disabled; or the associated user account is disabled |
| 500 | `{"message": "Internal server error"}` | Unexpected server error |

### Step 2: Estimate Price (Recommended)

Find out what the offer will cost before committing: how many Ad Units it uses, and what those units cost in money. This is especially important if you intend to pay from balance (payment method `B`), so you can confirm the balance is sufficient.

**Endpoint:** `POST` `/offers/price-estimate`

**Price formula:** `Ad Units = duration in hours × number of matching Agilis users`, rounded to 2 decimal places. `Cost = Ad Units × the Ad Unit price`, rounded to 2 decimal places.

**The Ad Unit price** is set in pounds sterling: £0.001. In any other currency it is £0.001 × that day's European Central Bank reference rate for the currency, rounded to 8 decimal places. For example, at 1.3412 US dollars to the pound one Ad Unit costs US$0.001341. Rates are refreshed each working day.

**Your billing currency.** Every organisation is billed in one currency, chosen when it registers. Your balance, every payment, every refund of Unused Time and every invoice are in it, and every price the API quotes or charges is in it. There is no currency parameter: you cannot choose a different currency per offer, and the billing currency cannot be changed through the API: once your organisation is approved, only Agilis support can change it.

**Ad Units and money are different things.** `price_estimate` is a number of Ad Units. Your balance, and the amount an offer deducts from it, is money in your billing currency. Compare your balance with `estimated_cost`, never with `price_estimate`: for an account in pounds the two differ by a factor of 100.

A user matches when they are within the target radius *and* satisfy any `gender` and `interests` targeting you supply. Only active, non-banned accounts are counted. Because targeting narrows the audience, it also lowers the price — pass the same `gender` and `interests` values you intend to use in Step 3, or the estimate will be higher than the amount actually charged.

```
curl -X POST "https://ads.agilis.dating/offers/price-estimate" \
  -H "x-api-key: YOUR_API_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "start_datetime": "2027-08-01T09:00:00Z",
    "end_datetime":   "2027-08-31T23:59:00Z",
    "location": [51.5074, -0.1278],
    "radius": 10,
    "gender": ["Male", "Female"],
    "interests": [3, 7]
  }'
```

#### Parameters

| Field | Type | Required | Notes |
|---|---|---|---|
| `start_datetime` | string (ISO 8601) | Yes | ISO 8601, UTC. Now or later: a start more than 60 seconds before now (UTC) is refused with `E1`. Example: `"2027-07-10T08:00:00Z"` |
| `end_datetime` | string (ISO 8601) | Yes | ISO 8601, UTC. Example: `"2027-07-12T20:30:00Z"` |
| `location` | array of 2 numbers | Yes | Latitude and longitude. Example: `[51.5074, -0.1278]` |
| `radius` | integer | Yes | Miles, minimum 5, maximum 30. Example: `10` |
| `gender` | array of strings | No | One or more of `Male`, `Female`, `Non-binary`, `All`. Omit, or include `All`, to reach every gender. Example: `["Male", "Female"]` |
| `interests` | array of integers | No | Interest ids from `GET /interests`. A user matches if they share any of them. Omit to apply no interest filter. Example: `[3, 7]` |
| `environment` | string | No | `TEST` or `LIVE`. Defaults to your API key's own mode. If sent, it must be your key's mode (HTTP 403 otherwise). The Ad Unit price is the same in both. |

#### Successful response — HTTP 200, S1

```
{
  "return_code": "S1",
  "price_estimate": 1337669.67,
  "matching_users": 1820,
  "ad_units": 1337669.67,
  "ad_unit_price": 0.001,
  "estimated_cost": 1337.67,
  "currency": "GBP",
  "exchange_rate_per_gbp": 1,
  "exchange_rate_date": null
}
```

The same offer for an organisation billed in US dollars, on a day when the rate was 1.3412:

```
{
  "return_code": "S1",
  "price_estimate": 1337669.67,
  "matching_users": 1820,
  "ad_units": 1337669.67,
  "ad_unit_price": 0.001341,
  "estimated_cost": 1794.08,
  "currency": "USD",
  "exchange_rate_per_gbp": 1.3412,
  "exchange_rate_date": "2027-09-29"
}
```

| Field | Meaning |
|---|---|
| `price_estimate` | The offer's size in **Ad Units**. Not money. Here: 1,820 users × 734.98 hours |
| `ad_units` | The same figure under a clearer name. `price_estimate` is kept for existing integrations |
| `matching_users` | The number of Agilis users the offer would reach with the targeting supplied. `0` means the offer cannot be created as specified: widen the radius or broaden the targeting |
| `ad_unit_price` | The price of one Ad Unit in `currency`, to 6 decimal places |
| `estimated_cost` | **The money the offer will cost**, in `currency`: the amount deducted from your balance, or charged to your card (plus VAT where it applies). This is the figure to compare with your balance |
| `currency` | Your organisation's billing currency: the currency of `ad_unit_price` and `estimated_cost` |
| `exchange_rate_per_gbp` | The European Central Bank reference rate the price was converted at: units of `currency` per pound sterling. `1` for GBP |
| `exchange_rate_date` | The date of that rate, `YYYY-MM-DD`. `null` for GBP |

If your billing currency cannot be billed right now, the estimate still returns `S1` with the Ad Unit figures, and `ad_unit_price`, `estimated_cost`, `exchange_rate_per_gbp` and `exchange_rate_date` are `null`. That happens when the currency's exchange rate is out of date (try again later), or when your organisation registered in a currency Agilis does not bill in (an Org Admin can choose a supported billing currency in the Ad Portal). `POST /offers` refuses both cases with `E6`.

#### Error responses

| Code | HTTP | Meaning |
|---|---|---|
| `S1` | 200 | Estimate calculated successfully. |
| `E1` | 400 | One or more inputs are missing or invalid, or `start_datetime` is in the past (more than 60 seconds before now). |
| `E2` | 400 | A date/time is not in valid ISO 8601 format. |
| `E1` | 403 | `environment` is not your API key's mode. |
| `401` | 401 | Authentication failure. |

### Step 3: Create the Offer

Submit the offer as a `multipart/form-data` request. The image must be sent as a binary file stream. The API runs automated content moderation on the image before the offer is created. If moderation fails, the offer is not created and no charge is made.

**Endpoint:** `POST` `/offers`

#### Parameters

| Field | Type | Required | Notes |
|---|---|---|---|
| `Idempotency-Key` header | string | Yes | A unique value you generate per offer (for example a UUID v4). Replaying the same key safely retries the request without creating a duplicate offer or a second Stripe payment; the original response, including any `payment_url`, is returned verbatim. Missing: HTTP 400 / `E1`. |
| `category_id` | integer | Yes | A valid category ID from the Category List API. Example: `1` |
| `title` | string | Yes | Up to 40 characters. Example: `"20% off summer sale"` |
| `description` | string | Yes | Up to 500 characters. Example: `"Discount valid through July"` |
| `terms_and_conditions` | string | No | Up to 500 characters. Example: `"Valid online only."` |
| `advertised_name` | string | No | The business name members see on the offer instead of your organisation's trading name — for example when an agency advertises for a client. Up to 100 characters; must not start with http://, https:// or www.; no HTML or line breaks. Omit or send empty to show your trading name. You must be authorised by the business you name. Invoices and analytics stay in your organisation's name. Example: `"Brighton Bakehouse"` |
| `url` | string (URL) | Yes | Your offer page. Example: `"https://advertiseroffer.com"` |
| `image` | file | Yes | JPG, PNG or GIF, maximum 5 MB, sent as a binary file stream. Recommended size: `1080x1080px` |
| `location[]` | array of 2 numbers | Yes | Latitude and longitude as two values. Example: `[51.5074, -0.1278]` |
| `radius` | integer | Yes | Radius in miles, minimum 5, maximum 30. Example: `10` |
| `gender[]` | array of strings | No | Target specific genders. One or more of `Male`, `Female`, `Non-binary`, `All`. Omitting the field, or including `All`, reaches every gender. Narrowing this reduces the audience and therefore the price. Example: `["Male", "Female"]` |
| `interests[]` | array of integers | No | Target users by interest, using ids from `GET /interests`. A user matches if they share **any** of the interests supplied. Omitting the field applies no interest filter. Narrowing this reduces the audience and therefore the price. Example: `[3, 7]` |
| `start_datetime` | string (ISO 8601) | Yes | ISO 8601, UTC. Now or later: a start more than 60 seconds before now (UTC) is refused with `E1`. Example: `"2027-07-10T08:00:00Z"` |
| `expiry_datetime` | string (ISO 8601) | Yes | ISO 8601, UTC; at least 60 seconds after `start_datetime`. Example: `"2027-07-12T20:30:00Z"` |
| `payment_method` | string | Yes | `E` = Stripe payment link by email; `B` = deduct from organisation balance (the balance of the offer's mode: TEST and LIVE balances are separate). For `E`, an organisation registered in the UK is charged VAT at 20% on top, as a separate line on the Stripe payment page. The offer's price and balance stay net of VAT. |
| `environment` | string | Yes | `TEST` or `LIVE`, in capitals. Must be your API key's own mode: a `pk_live_` key creates LIVE offers only, a `pk_test_` key TEST offers only. |

> **Idempotency (required):** Every `POST /offers` request must include an `Idempotency-Key` header — a unique value you generate (e.g. a UUID v4). Replaying the same key safely retries the request without creating a duplicate offer or a second Stripe payment; the original response (including any `payment_url`) is returned verbatim. A request with no `Idempotency-Key` header is rejected with `HTTP 400` / `E1`.

#### Path A — Payment by Stripe link (`payment_method=E`)

The offer is created in **Awaiting Payment** status. A Stripe Checkout link is generated and returned. The organisation admin receives the link and completes payment externally. When Stripe confirms payment via webhook, the offer is automatically activated.

```
curl -X POST "https://ads.agilis.dating/offers" \
  -H "x-api-key: YOUR_API_KEY_HERE" \
  -H "Idempotency-Key: 3f1a9c7e-1b2d-4f9a-8c0e-2a6b5d4e7f10" \
  -F "title=Summer Sale — 20% Off" \
  -F "description=Discount valid through August on all summer items." \
  -F "terms_and_conditions=Valid for online purchases only. One per customer." \
  -F "category_id=1" \
  -F "url=https://www.example.com/summer-sale" \
  -F "location[]=51.5074" \
  -F "location[]=-0.1278" \
  -F "radius=10" \
  -F "gender[]=Male" \
  -F "gender[]=Female" \
  -F "interests[]=3" \
  -F "interests[]=7" \
  -F "start_datetime=2027-08-01T09:00:00Z" \
  -F "expiry_datetime=2027-08-31T23:59:00Z" \
  -F "payment_method=E" \
  -F "environment=LIVE" \
  -F "image=@/path/to/offer-image.jpg"
```

##### Response immediately after creation — HTTP 200, S1

The offer is created but not yet active. Store the `offer_id` and present the `payment_url` to the user.

```
{
  "return_code": "S1",
  "offer_id": 45,
  "status": "Awaiting Payment",
  "payment_url": "https://checkout.stripe.com/c/pay/cs_live_...",
  "currency": "GBP"
}
```

`currency` is your organisation's billing currency, the currency the card is charged in.

Direct your user to this Stripe Checkout page. Your user enters their card details and completes payment on Stripe's hosted page. The page shows one line for the offer, at the offer's price (the `estimated_cost` from Step 2), in your billing currency.

**Minimum card payment:** Stripe will not take a card payment below its minimum for the currency (for example £0.30, US$0.50 or €0.50). A card offer priced below it is refused with `E1`, and the message names the minimum: widen the audience, lengthen the offer, or pay from your balance.

**VAT:** for an organisation registered in the UK, the Stripe page adds VAT at 20% as a separate line, so the card is charged the offer's price plus VAT. Organisations registered anywhere else are charged no VAT. The offer's price, and any Unused Time later credited to your balance, are always net of VAT.

After payment, Stripe sends a `checkout.session.completed` webhook to Agilis, which activates the offer automatically. The status becomes **Active** if `start_datetime` is now or in the past, or **Scheduled** if it is in the future.

> **Note:** If the user closes the Stripe page without completing payment, the offer remains in **Awaiting Payment** status. The platform still activates the offer the moment Stripe confirms payment — no action is required from you if the user later returns and completes the checkout.

#### Path B — Payment from organisation balance (`payment_method=B`)

Before calling this path, confirm that your organisation's balance is at least the `estimated_cost` returned in Step 2. That is the money figure; do not compare the balance with `price_estimate`, which is a number of Ad Units. If the balance is insufficient, the API returns `E3` and no offer is created. Payment is deducted immediately when the offer is created; no Stripe link is involved.

The balance used is the one for your API key's mode. **TEST and LIVE balances are separate:** a TEST offer is paid from the TEST balance, a LIVE offer from the LIVE balance, and a TEST top-up can never pay for a LIVE offer. No VAT is added to a balance payment; where VAT applies, it was charged when the balance was topped up.

```
curl -X POST "https://ads.agilis.dating/offers" \
  -H "x-api-key: YOUR_API_KEY_HERE" \
  -H "Idempotency-Key: 3f1a9c7e-1b2d-4f9a-8c0e-2a6b5d4e7f10" \
  -F "title=Summer Sale — 20% Off" \
  -F "description=Discount valid through August on all summer items." \
  -F "terms_and_conditions=Valid for online purchases only. One per customer." \
  -F "category_id=1" \
  -F "url=https://www.example.com/summer-sale" \
  -F "location[]=51.5074" \
  -F "location[]=-0.1278" \
  -F "radius=10" \
  -F "gender[]=Male" \
  -F "gender[]=Female" \
  -F "interests[]=3" \
  -F "interests[]=7" \
  -F "start_datetime=2027-08-01T09:00:00Z" \
  -F "expiry_datetime=2027-08-31T23:59:00Z" \
  -F "payment_method=B" \
  -F "environment=LIVE" \
  -F "image=@/path/to/offer-image.jpg"
```

##### Response when start date is in the future — HTTP 200, S1

```
{
  "return_code": "S1",
  "offer_id": 46,
  "status": "Scheduled",
  "payment_url": null
}
```

##### Response when start date has already passed — HTTP 200, S1

```
{
  "return_code": "S1",
  "offer_id": 47,
  "status": "Active",
  "payment_url": null
}
```

The price of the Ad Units, in your organisation's billing currency, is deducted from your balance and your organisation's balance, and Org Admins are notified by email in both cases.

#### Create Offer error responses (both payment paths)

| Code | HTTP | Meaning |
|---|---|---|
| `S1` | 200 | Offer created: payment link issued (`E`), or Active/Scheduled if paid from balance (`B`). |
| `E1` | 400 | One or more inputs are missing or invalid: an invalid datetime, a duration under 60 seconds, a radius outside 5 to 30 miles, an oversized image or wrong file type, an unrecognised `gender` value, an unknown `interests` id, or an `advertised_name` over 100 characters, starting with http://, https:// or www., or containing HTML or a line break. |
| `E1` | 400 | `start_datetime` is more than 60 seconds before now (UTC): nothing created or charged. Message: "start_datetime cannot be in the past. Use the current time or later." |
| `E1` | 400 | A card payment (`payment_method=E`) below Stripe's minimum for your currency: nothing created; the message names the minimum. |
| `E1` | 400 | The `Idempotency-Key` header is missing. Message: "Idempotency-Key header is required." |
| `E2` | 200 | The image failed automated content moderation: nothing created. Choose a different image. |
| `E3` | 200 | The organisation's balance is less than the offer's cost (`payment_method=B` only): top up and retry. |
| `E4` | 400 | Offer quota limit exceeded: the user or organisation has reached its maximum number of offers. |
| `E5` | 400 | No Agilis users match the targeting supplied: nothing created. Widen the radius or broaden the `gender` or `interests` targeting. |
| `E6` | 400 | Your billing currency cannot be billed right now: nothing created or charged. The message is either `Your organisation's currency (XYZ) is not one Agilis bills in. An Org Admin can choose a supported billing currency in the Ad Portal. You have not been charged.` or `Prices in XYZ are temporarily unavailable because the exchange rate is out of date. Please try again later. You have not been charged.`, with your currency code in place of XYZ. |
| `E1` | 403 | `environment` is not your API key's mode: nothing created or charged. Use a key created in that mode. |
| `E9` | 403 | Your organisation has not accepted the current Agilis Advertising Terms and Conditions: nothing created or charged. An Org Admin must accept them in the Ad Portal ([adportal.agilis.dating](https://adportal.agilis.dating)); then retry with the same `Idempotency-Key`. |
| `409` | 409 | The `Idempotency-Key` was reused while the first request is still being processed: retry shortly. |
| `422` | 422 | The `Idempotency-Key` was reused with a different payload. |
| `401` | 401 | Authentication failure. |
| `500` | 500 | Internal server error. |

### Step 4: Verify the Offer Was Created

Confirm creation and check the current status by calling View Offers.

```
curl -X GET "https://ads.agilis.dating/offers?environment=live" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### `offer_status` values

| Value | Meaning |
|---|---|
| `Awaiting Payment` | Created via method E; payment not yet received |
| `Active` | Live and visible to Agilis users |
| `Scheduled` | Paid for but start date is in the future |
| `Paused` | Temporarily paused by the advertiser |
| `Ended` | Expiry datetime has passed |

The `environment` query parameter is mandatory; use `test` or `live` (case-insensitive). It must be your API key's own mode: naming the other mode returns HTTP 403.

> **Pagination:** `GET /offers` returns a paginated response shaped as `{ "data": […], "pagination": { "limit", "count", "has_more", "next_cursor" } }`. Page with `limit` (default 20, max 100) and `cursor`, and optionally filter with `offer_status`, `created_after` and `created_before`. Each offer also carries a `status` field (`enabled` or `disabled`); deleted offers are never returned.

### Managing an Offer After Creation

Use the pause, resume, analytics, and delete endpoints to manage an offer once it exists. Valid analytics `interval` values are `24h`, `7d`, `30d`, `60d`, `90d`, and `all` (mandatory). Deleting an offer in Awaiting Payment status closes its Stripe payment page first; deleting a paid offer credits its Unused Time to your balance. Full request and response details for each are documented in the [Pause and Resume](https://www.agilis.dating/api-doc#pauseResumeOfferSectionid), [Analytics](https://www.agilis.dating/api-doc#analyticsSectionid), and [Delete Offer](https://www.agilis.dating/api-doc#deleteOfferSectionid) sections below.



### Environment Testing

Use a TEST key (created while the Ad Portal is in TEST mode) and set `environment=TEST` (in capitals) in the Create Offer request to create offers in a sandbox. Test offers are only returned when calling `GET /offers?environment=test` with a TEST key, do not appear to real Agilis users in the app, and follow the same creation, payment, and management flow as live offers. Card payments for TEST offers run in Stripe's test mode, and balance payments use the separate TEST balance. When your integration is ready, switch to a LIVE key: the Ad Portal's test/live mode toggle decides which kind of key you create.

## Offers endpoints

### GET /categories: Category List

Returns all active categories available to the organisation that owns the API key. It is typically called before creating an offer, because offer creation requires a valid category ID returned by this endpoint.

- Auth: header `x-api-key`

#### Response fields

| Field | Type | Meaning |
|---|---|---|
| `category_id` | integer | Unique ID of the category. Example: `1` |
| `category_name` | string | Name of the category. Example: `"Retail"` |

#### Response codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `200` | 200 | Categories retrieved successfully. |
| `401` | 401 | Invalid API key or disabled account. |
| `500` | 500 | Internal server error. |

#### Example request

```bash
curl -X GET "https://ads.agilis.dating/categories" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Example response

```json
{
  "categories": [
    {
      "category_id": 1,
      "category_name": "Retail"
    },
    {
      "category_id": 2,
      "category_name": "Food & Beverage"
    }
  ]
}
```

### GET /interests: Interest List

Returns the interests available for offer targeting. Call it before creating an offer if you intend to use the optional `interests` array, which restricts the offer to users who have selected at least one of the interests supplied.

- Auth: header `x-api-key`

#### Response fields

| Field | Type | Meaning |
|---|---|---|
| `interest_id` | integer | Unique ID of the interest. Example: `3` |
| `interest_name` | string | Name of the interest. Example: `"Cooking"` |

#### Response codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `200` | 200 | Interests retrieved successfully. |
| `401` | 401 | Invalid API key or disabled account. |
| `500` | 500 | Internal server error. |

#### Example request

```bash
curl -X GET "https://ads.agilis.dating/interests" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Example response

```json
{
  "interests": [
    {
      "interest_id": 3,
      "interest_name": "Cooking"
    },
    {
      "interest_id": 7,
      "interest_name": "Fitness"
    }
  ]
}
```

### POST /offers/price-estimate: Offer Price Estimate

Calculates what an offer will cost before it is created: its size in Ad Units (duration in hours × the number of Agilis users it would reach) and what those units cost in money. Only active, non-banned accounts are counted. Pass the same `gender` and `interests` values you intend to create the offer with, otherwise the estimate will be higher than the amount actually charged.

`price_estimate` is a number of **Ad Units**, not money. `estimated_cost` is the **money** the offer will cost, and is the figure to compare with your balance.

There is no `currency` parameter: the estimate is always in your organisation's billing currency, named in the response's `currency` field.

- Auth: header `x-api-key`
- Content-Type: `application/json`

#### Parameters

| Field | In | Type | Required | Notes |
|---|---|---|---|---|
| `start_datetime` | body | string (ISO 8601) | yes | ISO 8601, UTC. Now or later: a start more than 60 seconds before now (UTC) is refused with `E1`. Example: `"2027-07-10T08:00:00Z"` |
| `end_datetime` | body | string (ISO 8601) | yes | ISO 8601, UTC. Example: `"2027-07-12T20:30:00Z"` |
| `location` | body | array of 2 numbers | yes | Latitude and longitude. Example: `[51.5074, -0.1278]` |
| `radius` | body | integer | yes | Miles, minimum 5, maximum 30. Example: `10` |
| `gender` | body | array of strings | no | One or more of `Male`, `Female`, `Non-binary`, `All`. Omit, or include `All`, to reach every gender. Example: `["Male", "Female"]` |
| `interests` | body | array of integers | no | Interest ids from `GET /interests`. A user matches if they share any of them. Omit to apply no interest filter. Example: `[3, 7]` |
| `environment` | body | string | no | `TEST` or `LIVE`. Defaults to your API key's own mode. If sent, it must be your key's mode (HTTP 403 otherwise). The Ad Unit price is the same in both. |

#### Response fields

| Field | Type | Meaning |
|---|---|---|
| `price_estimate` | number | Size of the offer in **Ad Units**. Not money. |
| `ad_units` | number | The same figure, under a clearer name. `price_estimate` is kept for existing integrations. |
| `matching_users` | integer | Agilis users the offer would reach. `0` means it cannot be created as specified: widen the radius or broaden the targeting. |
| `ad_unit_price` | number|null | Price of one Ad Unit in `currency`, to 6 decimal places: £0.001 × the day's European Central Bank rate. |
| `estimated_cost` | number|null | **The money the offer will cost.** Compare this with your balance. |
| `currency` | string | Your organisation's billing currency. |
| `exchange_rate_per_gbp` | number|null | The rate used, per pound sterling. `1` for GBP. |
| `exchange_rate_date` | string|null | The rate's date (`YYYY-MM-DD`); `null` for GBP. |

If your billing currency cannot be billed right now (not one Agilis bills in, or its rate is out of date), `ad_unit_price`, `estimated_cost`, `exchange_rate_per_gbp` and `exchange_rate_date` are `null`; the Ad Unit figures are still returned, and `POST /offers` would refuse the offer with `E6`.

#### Return codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `S1` | 200 | Estimate calculated successfully. |
| `E1` | 400 | One or more inputs are missing or invalid, or `start_datetime` is in the past (more than 60 seconds before now). |
| `E2` | 400 | A date/time is not in valid ISO 8601 format. |
| `E1` | 403 | `environment` is not your API key's mode. |
| `401` | 401 | Authentication failure. |

#### Example request

```bash
curl -X POST "https://ads.agilis.dating/offers/price-estimate" \
  -H "x-api-key: YOUR_API_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "start_datetime": "2027-07-10T08:00:00Z",
    "end_datetime": "2027-07-12T20:30:00Z",
    "location": [51.5074, -0.1278],
    "radius": 10,
    "gender": ["Male", "Female"],
    "interests": [3, 7]
  }'
```

#### Example response

```json
{
  "return_code": "S1",
  "price_estimate": 110110,
  "matching_users": 1820,
  "ad_units": 110110,
  "ad_unit_price": 0.001,
  "estimated_cost": 110.11,
  "currency": "GBP",
  "exchange_rate_per_gbp": 1,
  "exchange_rate_date": null
}
```

### POST /offers: Create Offer

Creates a new offer for the authenticated advertiser. The request is sent as `multipart/form-data` with the offer image supplied as a binary file stream. The image is checked by automated content moderation before the offer is created: if it fails, nothing is created and nothing is charged.

**Paying by Stripe link** (`payment_method=E`): the offer is created in **Awaiting Payment** and the response carries a `payment_url`. Stripe's `checkout.session.completed` webhook activates it: **Active** if its start has passed, **Scheduled** if not. An organisation registered in the UK pays VAT at 20% on top, as a separate line on the Stripe page. Stripe will not take a card payment below its minimum for the currency (for example £0.30, US$0.50 or €0.50): such an offer is refused with `E1`.

**Paying from balance** (`payment_method=B`): the offer's price is deducted at once, from the balance of the offer's own mode (TEST and LIVE balances are separate). Compare your balance with `estimated_cost` from the price estimate, never `price_estimate`.

There is no `currency` parameter: every offer is priced and charged in your organisation's billing currency, named in the response.

- Auth: header `x-api-key`
- Content-Type: `multipart/form-data`

#### Parameters

| Field | In | Type | Required | Notes |
|---|---|---|---|---|
| `Idempotency-Key` | header | string | yes | A unique value you generate per offer (for example a UUID v4). Replaying the same key safely retries the request without creating a duplicate offer or a second Stripe payment; the original response, including any `payment_url`, is returned verbatim. Missing: HTTP 400 / `E1`. |
| `category_id` | body | integer | yes | A valid category ID from the Category List API. Example: `1` |
| `title` | body | string | yes | Up to 40 characters. Example: `"20% off summer sale"` |
| `description` | body | string | yes | Up to 500 characters. Example: `"Discount valid through July"` |
| `terms_and_conditions` | body | string | no | Up to 500 characters. Example: `"Valid online only."` |
| `advertised_name` | body | string | no | The business name members see on the offer instead of your organisation's trading name — for example when an agency advertises for a client. Up to 100 characters; must not start with http://, https:// or www.; no HTML or line breaks. Omit or send empty to show your trading name. You must be authorised by the business you name. Invoices and analytics stay in your organisation's name. Example: `"Brighton Bakehouse"` |
| `url` | body | string (URL) | yes | Your offer page. Example: `"https://advertiseroffer.com"` |
| `image` | body | file | yes | JPG, PNG or GIF, maximum 5 MB, sent as a binary file stream. Recommended size: `1080x1080px` |
| `location[]` | body | array of 2 numbers | yes | Latitude and longitude as two values. Example: `[51.5074, -0.1278]` |
| `radius` | body | integer | yes | Radius in miles, minimum 5, maximum 30. Example: `10` |
| `gender[]` | body | array of strings | no | Target specific genders. One or more of `Male`, `Female`, `Non-binary`, `All`. Omitting the field, or including `All`, reaches every gender. Narrowing this reduces the audience and therefore the price. Example: `["Male", "Female"]` |
| `interests[]` | body | array of integers | no | Target users by interest, using ids from `GET /interests`. A user matches if they share **any** of the interests supplied. Omitting the field applies no interest filter. Narrowing this reduces the audience and therefore the price. Example: `[3, 7]` |
| `start_datetime` | body | string (ISO 8601) | yes | ISO 8601, UTC. Now or later: a start more than 60 seconds before now (UTC) is refused with `E1`. Example: `"2027-07-10T08:00:00Z"` |
| `expiry_datetime` | body | string (ISO 8601) | yes | ISO 8601, UTC; at least 60 seconds after `start_datetime`. Example: `"2027-07-12T20:30:00Z"` |
| `payment_method` | body | string | yes | `E` = Stripe payment link by email; `B` = deduct from organisation balance (the balance of the offer's mode: TEST and LIVE balances are separate). For `E`, an organisation registered in the UK is charged VAT at 20% on top, as a separate line on the Stripe payment page. The offer's price and balance stay net of VAT. |
| `environment` | body | string | yes | `TEST` or `LIVE`, in capitals. Must be your API key's own mode: a `pk_live_` key creates LIVE offers only, a `pk_test_` key TEST offers only. |

#### Return codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `S1` | 200 | Offer created: payment link issued (`E`), or Active/Scheduled if paid from balance (`B`). |
| `E1` | 400 | One or more inputs are missing or invalid: an invalid datetime, a duration under 60 seconds, a radius outside 5 to 30 miles, an oversized image or wrong file type, an unrecognised `gender` value, an unknown `interests` id, or an `advertised_name` over 100 characters, starting with http://, https:// or www., or containing HTML or a line break. |
| `E1` | 400 | `start_datetime` is more than 60 seconds before now (UTC): nothing created or charged. Message: "start_datetime cannot be in the past. Use the current time or later." |
| `E1` | 400 | A card payment (`payment_method=E`) below Stripe's minimum for your currency: nothing created; the message names the minimum. |
| `E1` | 400 | The `Idempotency-Key` header is missing. Message: "Idempotency-Key header is required." |
| `E2` | 200 | The image failed automated content moderation: nothing created. Choose a different image. |
| `E3` | 200 | The organisation's balance is less than the offer's cost (`payment_method=B` only): top up and retry. |
| `E4` | 400 | Offer quota limit exceeded: the user or organisation has reached its maximum number of offers. |
| `E5` | 400 | No Agilis users match the targeting supplied: nothing created. Widen the radius or broaden the `gender` or `interests` targeting. |
| `E6` | 400 | Your billing currency cannot be billed right now: nothing created or charged. The message is either `Your organisation's currency (XYZ) is not one Agilis bills in. An Org Admin can choose a supported billing currency in the Ad Portal. You have not been charged.` or `Prices in XYZ are temporarily unavailable because the exchange rate is out of date. Please try again later. You have not been charged.`, with your currency code in place of XYZ. |
| `E1` | 403 | `environment` is not your API key's mode: nothing created or charged. Use a key created in that mode. |
| `E9` | 403 | Your organisation has not accepted the current Agilis Advertising Terms and Conditions: nothing created or charged. An Org Admin must accept them in the Ad Portal ([adportal.agilis.dating](https://adportal.agilis.dating)); then retry with the same `Idempotency-Key`. |
| `409` | 409 | The `Idempotency-Key` was reused while the first request is still being processed: retry shortly. |
| `422` | 422 | The `Idempotency-Key` was reused with a different payload. |
| `401` | 401 | Authentication failure. |
| `500` | 500 | Internal server error. |

#### Example request

```bash
curl -X POST "https://ads.agilis.dating/offers" \
  -H "x-api-key: YOUR_API_KEY_HERE" \
  -H "Idempotency-Key: 3f1a9c7e-1b2d-4f9a-8c0e-2a6b5d4e7f10" \
  -F "category_id=1" \
  -F "title=20% off summer sale" \
  -F "description=Discount valid through July" \
  -F "terms_and_conditions=Valid online only." \
  -F "url=https://advertiseroffer.com" \
  -F "location[]=51.5074" \
  -F "location[]=-0.1278" \
  -F "radius=10" \
  -F "gender[]=Male" \
  -F "gender[]=Female" \
  -F "interests[]=3" \
  -F "interests[]=7" \
  -F "start_datetime=2027-07-10T08:00:00Z" \
  -F "expiry_datetime=2027-07-12T20:30:00Z" \
  -F "payment_method=E" \
  -F "environment=LIVE" \
  -F "image=@/path/to/offer.jpg"
```

#### Example response

```json
{
  "return_code": "S1",
  "offer_id": 45,
  "status": "Awaiting Payment",
  "payment_url": "https://checkout.stripe.com/c/pay/cs_live_...",
  "currency": "GBP"
}

// payment_method=B, start in the future (HTTP 200, S1):
// { "return_code": "S1", "offer_id": 46, "status": "Scheduled", "payment_url": null }
```

### GET /offers: View Offers

Returns a **paginated** list of offers created by the authenticated advertiser for the requested environment, newest first. Offers whose image was rejected by content moderation are never created, so they are not returned, and deleted offers are excluded. Page through results with `limit` and `cursor`.

- Auth: header `x-api-key`

#### Parameters

| Field | In | Type | Required | Notes |
|---|---|---|---|---|
| `environment` | query | string | yes | `test` or `live` (case-insensitive). Must be your API key's own mode. Example: `/offers?environment=test` |
| `limit` | query | integer | no | Page size. Default `20`, maximum `100`. |
| `cursor` | query | string | no | Opaque token from the previous page's `next_cursor`. Omit for the first page. |
| `offer_status` | query | string | no | Lifecycle filter: `awaiting_payment`, `active`, `paused` or `ended` (case-insensitive). |
| `created_after` | query | string (ISO 8601) | no | ISO 8601, UTC. Offers created at or after this time. |
| `created_before` | query | string (ISO 8601) | no | ISO 8601, UTC. Offers created at or before this time. |

#### Response fields

| Field | Type | Meaning |
|---|---|---|
| `offer_id` | integer | The offer's id. |
| `title, category, description, terms_and_conditions, url` | string | As created. |
| `advertised_name` | string|null | The business name members see; `null` when your trading name is shown. |
| `image` | string | Signed CloudFront URL of the image. |
| `latitude, longitude, radius` | number | Where the offer is shown. |
| `gender` | array | For example `["Male","Female"]`; empty when the offer targets every gender. |
| `interests` | array | `{interest_id, interest_name}` objects; empty when no interest targeting was applied. |
| `start_datetime, expiry_datetime, creation_datetime` | string | ISO 8601, UTC. |
| `offer_status` | string | Awaiting Payment, Active, Scheduled, Paused or Ended. |
| `status` | string | `enabled` or `disabled`: whether an Admin has disabled the offer. |

#### Response codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `200` | 200 | Page returned (empty `data` array if none match). |
| `400` | 400 | Invalid query parameter (`limit`, `cursor`, `offer_status` or date). |
| `401` | 401 | Invalid API key or disabled account. |
| `403` | 403 | `environment` is not your API key's mode. |
| `500` | 500 | Internal server error. |

#### Example request

```bash
curl -X GET "https://ads.agilis.dating/offers?environment=test&limit=20" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Example response

```json
{
  "data": [
    {
      "offer_id": 45,
      "title": "20% off summer sale",
      "category": "Retail",
      "description": "Discount valid through July",
      "terms_and_conditions": "Valid online only.",
      "advertised_name": null,
      "url": "https://advertiseroffer.com",
      "image": "https://ads.agilis.dating/92/offers/45/offer_orig.png",
      "latitude": 51.5074,
      "longitude": -0.1278,
      "radius": 10,
      "gender": ["Male", "Female"],
      "interests": [
        {
          "interest_id": 3,
          "interest_name": "Cooking"
        },
        {
          "interest_id": 7,
          "interest_name": "Fitness"
        }
      ],
      "start_datetime": "2027-07-10T08:00:00Z",
      "expiry_datetime": "2027-07-12T20:30:00Z",
      "creation_datetime": "2027-06-01T10:00:00Z",
      "offer_status": "Active",
      "status": "enabled"
    }
  ],
  "pagination": {
    "limit": 20,
    "count": 1,
    "has_more": false,
    "next_cursor": null
  }
}
```

### POST /offers/{offer_id}/pause: Pause Offer

Pauses a running offer. The offer must belong to the account that owns the API key and be in the key's mode (TEST or LIVE). Any other offer returns HTTP 401 with `E1`. Time spent paused is Unused Time: it is credited to your balance if you later delete the offer.

- Auth: header `x-api-key`

#### Parameters

| Field | In | Type | Required | Notes |
|---|---|---|---|---|
| `offer_id` | path | integer | yes | Path parameter. Example: `45` |

#### Return codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `S1` | 200 | Offer paused successfully. |
| `E1` | 401 | Invalid API key, disabled account, or the offer does not belong to this account or is in the other mode. |
| `E2` | 200 | Offer has already ended. |
| `E3` | 200 | Offer is already paused. |
| `E5` | 200 | Offer is awaiting payment. |

#### Example request

```bash
curl -X POST "https://ads.agilis.dating/offers/45/pause" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Example response

```json
{
  "return_code": "S1",
  "message": "Offer has been paused."
}
```

### POST /offers/{offer_id}/resume: Resume Offer

Resumes a paused offer. The offer must belong to the account that owns the API key and be in the key's mode. Any other offer returns HTTP 401 with `E1`.

- Auth: header `x-api-key`

#### Parameters

| Field | In | Type | Required | Notes |
|---|---|---|---|---|
| `offer_id` | path | integer | yes | Path parameter. Example: `45` |

#### Return codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `S2` | 200 | Offer resumed successfully. |
| `E1` | 401 | Invalid API key, disabled account, or the offer does not belong to this account or is in the other mode. |
| `E2` | 200 | Offer has already ended. |
| `E4` | 200 | Offer is already running. |
| `E5` | 200 | Offer is awaiting payment. |

#### Example request

```bash
curl -X POST "https://ads.agilis.dating/offers/45/resume" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Example response

```json
{
  "return_code": "S2",
  "message": "Offer has been resumed."
}
```

### GET /offers/{offer_id}/analytics: View Offer Analytics

Returns view and click metrics for a specific offer over the requested time interval. The offer must belong to the account that owns the API key and be in the key's mode.

- Auth: header `x-api-key`

#### Parameters

| Field | In | Type | Required | Notes |
|---|---|---|---|---|
| `offer_id` | path | integer | yes | Path parameter. Example: `45` |
| `interval` | query | string | yes | One of `24h`, `7d`, `30d`, `60d`, `90d` or `all`. |

#### Response codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `200` | 200 | Analytics returned successfully. |
| `401` | 401 | Invalid API key, disabled account, or the offer does not belong to this account or is in the other mode. |
| `500` | 500 | Internal server error. |

#### Example request

```bash
curl -X GET "https://ads.agilis.dating/offers/45/analytics?interval=30d" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Example response

```json
{
  "offer_id": 45,
  "interval": "30d",
  "start_datetime": "2027-07-10T08:00:00Z",
  "end_datetime": "2027-08-09T08:00:00Z",
  "status": "Active",
  "metrics": {
    "views": 4789,
    "clicks": 1032
  }
}
```

### DELETE /offers/{offer_id}: Delete Offer

Deletes an offer belonging to the authenticated account, in your API key's mode (an offer of the other mode is not found).

**Awaiting Payment:** the offer's Stripe payment page is closed first, so it can no longer be paid. If Stripe reports that the payment has just been completed, or the page cannot be closed, nothing is deleted (`E6`, `E7` below).

**Paid offers** (Active, Scheduled or Paused): the offer's Unused Time is credited to your balance in the offer's own mode. Unused Time is the part of the scheduled period in which the offer did not run: the time from the deletion to the scheduled end, plus any time it spent paused. An offer deleted before it starts is credited in full. The credit is net of VAT and goes to your balance, never back to a card.

- Auth: header `x-api-key`

#### Parameters

| Field | In | Type | Required | Notes |
|---|---|---|---|---|
| `offer_id` | path | integer | yes | Path parameter. Example: `45` |

#### Return codes (this endpoint only)

| Code | HTTP | Meaning |
|---|---|---|
| `S1` | 200 | Offer deleted successfully. |
| `E1` | 401 | Invalid input, or "Offer not found": the offer does not belong to this account or is in the other mode. |
| `E6` | 409 | The payment for this offer has just been completed, so it is no longer awaiting payment. Nothing has been deleted. Retrieve the offer again; if you still delete it, its Unused Time is credited to your balance. |
| `E7` | 503 | The payment page could not be closed. Nothing has been deleted. Try again in a few minutes. |

#### Example request

```bash
curl -X DELETE "https://ads.agilis.dating/offers/45" \
  -H "x-api-key: YOUR_API_KEY_HERE"
```

#### Example response

```json
{
  "return_code": "S1",
  "message": "Offer has been deleted."
}
```

## Return codes by endpoint

A code's meaning depends on the endpoint: read the row for the endpoint you called.

| Code | Endpoint | HTTP | Meaning on that endpoint |
|---|---|---|---|
| `S1` | `POST /offers/price-estimate` | 200 | Estimate calculated successfully. |
| `S1` | `POST /offers` | 200 | Offer created: payment link issued (`E`), or Active/Scheduled if paid from balance (`B`). |
| `S1` | `POST /offers/{offer_id}/pause` | 200 | Offer paused successfully. |
| `S1` | `DELETE /offers/{offer_id}` | 200 | Offer deleted successfully. |
| `S2` | `POST /offers/{offer_id}/resume` | 200 | Offer resumed successfully. |
| `E1` | `POST /offers/price-estimate` | 400 | One or more inputs are missing or invalid, or `start_datetime` is in the past (more than 60 seconds before now). |
| `E1` | `POST /offers/price-estimate` | 403 | `environment` is not your API key's mode. |
| `E1` | `POST /offers` | 400 | One or more inputs are missing or invalid: an invalid datetime, a duration under 60 seconds, a radius outside 5 to 30 miles, an oversized image or wrong file type, an unrecognised `gender` value, an unknown `interests` id, or an `advertised_name` over 100 characters, starting with http://, https:// or www., or containing HTML or a line break. |
| `E1` | `POST /offers` | 400 | `start_datetime` is more than 60 seconds before now (UTC): nothing created or charged. Message: "start_datetime cannot be in the past. Use the current time or later." |
| `E1` | `POST /offers` | 400 | A card payment (`payment_method=E`) below Stripe's minimum for your currency: nothing created; the message names the minimum. |
| `E1` | `POST /offers` | 400 | The `Idempotency-Key` header is missing. Message: "Idempotency-Key header is required." |
| `E1` | `POST /offers` | 403 | `environment` is not your API key's mode: nothing created or charged. Use a key created in that mode. |
| `E1` | `POST /offers/{offer_id}/pause` | 401 | Invalid API key, disabled account, or the offer does not belong to this account or is in the other mode. |
| `E1` | `POST /offers/{offer_id}/resume` | 401 | Invalid API key, disabled account, or the offer does not belong to this account or is in the other mode. |
| `E1` | `DELETE /offers/{offer_id}` | 401 | Invalid input, or "Offer not found": the offer does not belong to this account or is in the other mode. |
| `E2` | `POST /offers/price-estimate` | 400 | A date/time is not in valid ISO 8601 format. |
| `E2` | `POST /offers` | 200 | The image failed automated content moderation: nothing created. Choose a different image. |
| `E2` | `POST /offers/{offer_id}/pause` | 200 | Offer has already ended. |
| `E2` | `POST /offers/{offer_id}/resume` | 200 | Offer has already ended. |
| `E3` | `POST /offers` | 200 | The organisation's balance is less than the offer's cost (`payment_method=B` only): top up and retry. |
| `E3` | `POST /offers/{offer_id}/pause` | 200 | Offer is already paused. |
| `E4` | `POST /offers` | 400 | Offer quota limit exceeded: the user or organisation has reached its maximum number of offers. |
| `E4` | `POST /offers/{offer_id}/resume` | 200 | Offer is already running. |
| `E5` | `POST /offers` | 400 | No Agilis users match the targeting supplied: nothing created. Widen the radius or broaden the `gender` or `interests` targeting. |
| `E5` | `POST /offers/{offer_id}/pause` | 200 | Offer is awaiting payment. |
| `E5` | `POST /offers/{offer_id}/resume` | 200 | Offer is awaiting payment. |
| `E6` | `POST /offers` | 400 | Your billing currency cannot be billed right now: nothing created or charged. The message is either `Your organisation's currency (XYZ) is not one Agilis bills in. An Org Admin can choose a supported billing currency in the Ad Portal. You have not been charged.` or `Prices in XYZ are temporarily unavailable because the exchange rate is out of date. Please try again later. You have not been charged.`, with your currency code in place of XYZ. |
| `E6` | `DELETE /offers/{offer_id}` | 409 | The payment for this offer has just been completed, so it is no longer awaiting payment. Nothing has been deleted. Retrieve the offer again; if you still delete it, its Unused Time is credited to your balance. |
| `E7` | `DELETE /offers/{offer_id}` | 503 | The payment page could not be closed. Nothing has been deleted. Try again in a few minutes. |
| `E9` | `POST /offers` | 403 | Your organisation has not accepted the current Agilis Advertising Terms and Conditions: nothing created or charged. An Org Admin must accept them in the Ad Portal ([adportal.agilis.dating](https://adportal.agilis.dating)); then retry with the same `Idempotency-Key`. |
