> ## Documentation Index
> Fetch the complete documentation index at: https://docs.play2sell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaigns API

> Read your company's incentive campaigns from your own backend — reward catalog, collaborator balance and what they already qualify for, built for super-app home screens and partner dashboards.

# Campaigns API

<Note>
  Examples below show the wire-format `Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE` header. To compute the signature in your code, use the `signedRequest` helper in [Authentication](/api/authentication).
</Note>

The Campaigns API lets your backend read your company's **incentive campaigns** in SalesOS — which ones are running, what the reward catalog offers, how much each collaborator has to spend, and how many rewards they already qualify for. It was built for **composing your own screens** before the user ever opens the SalesOS module.

It is a **read-only, server-to-server** API: you look collaborators up by CPF or email, in batch, and SalesOS answers with the running campaigns and each person's state in them.

<Note>
  A campaign is **configured by your company**: name, catalog, prices in points, categories, level rules and even the currency name. This API returns whatever is configured — it knows no specific campaign.
</Note>

## How It Works

1. **You get an API Key** with the `campaigns:read` scope (Admin > Integrations > API Keys)
2. **Your backend asks** for the state of one or many collaborators (by CPF or email)
3. **SalesOS answers** with the running campaigns — campaign data **once**, then each collaborator's state

<Note>
  Campaign metadata comes back **once at the top**, not repeated per person. On a 500-collaborator batch that is the difference between a lean response and a multi-megabyte one.
</Note>

<Warning>
  This endpoint does **not** redeem rewards or accept terms. It is read-only — pair it with a deep link into the SalesOS module for the action itself.
</Warning>

***

## Authentication

```
Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE
```

See the [Authentication page](/api/authentication) to create and manage API Keys.

| Property           | Details                                                  |
| ------------------ | -------------------------------------------------------- |
| **Header**         | `Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE` |
| **Scope required** | `campaigns:read`                                         |
| **Rate limit**     | Configurable per key (default: 1000 requests/hour)       |
| **Key format**     | `sk_live_` (production) or `sk_test_` (testing)          |

***

## Environments

<Tabs>
  <Tab title="Production">
    **Base URL:** `https://api.play2sell.com`
  </Tab>

  <Tab title="Staging">
    **Base URL:** `https://api-staging.play2sell.com`
  </Tab>
</Tabs>

***

## Endpoint Reference

```
POST https://api.play2sell.com/functions/v1/campaigns-partner-api
```

Two actions via the `action` field: `campaign_status` and `campaign_catalog`.

Each collaborator is identified by **CPF** (any format — digits are normalized) or **email**. When both are sent, CPF wins.

***

### Action: campaign\_status

Your company's running campaigns and each collaborator's state in them.

#### Request Schema

<ParamField body="action" type="string" required>
  Must be `"campaign_status"`
</ParamField>

<ParamField body="collaborators" type="array" required>
  Array of collaborator references (max 500)
</ParamField>

<ParamField body="collaborators[].cpf" type="string">
  CPF, any format (`52998224725` or `529.982.247-25`). Must contain exactly 11 digits.
</ParamField>

<ParamField body="collaborators[].email" type="string">
  Email — used only when `cpf` is absent
</ParamField>

<ParamField body="campaign_slug" type="string">
  Filter to a single campaign. Absent = every running campaign.
</ParamField>

<ParamField body="locale" type="string">
  Language for the configurable texts. Absent = the campaign's default.
</ParamField>

#### Example

```bash theme={null}
curl -X POST https://api.play2sell.com/functions/v1/campaigns-partner-api \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "campaign_status",
    "collaborators": [{ "cpf": "529.982.247-25" }]
  }'
```

**Response (200):**

```json theme={null}
{
  "data": {
    "timezone": "America/Sao_Paulo",
    "reference_date": "2026-08-12",
    "campaigns": [
      {
        "id": "aaaaaaaa-0000-0000-0000-000000000001",
        "slug": "your-campaign",
        "name": "Your Campaign",
        "tagline": "Your reward catalog",
        "status": "active",
        "default_locale": "pt",
        "locales": ["pt"],
        "currency": { "label": "pontos" },
        "catalog": {
          "rewards": 22,
          "awarded_only": 2,
          "categories": 6,
          "cheapest_points": 200,
          "priciest_points": 22000
        }
      }
    ],
    "collaborators": [
      {
        "cpf": "52998224725",
        "found": true,
        "user_id": "8f14e45f-0000-0000-0000-000000000001",
        "name": "Maria Santos",
        "membership_status": "active",
        "balance": {
          "spendable": 1500,
          "source": "playcoin",
          "wallet_enabled": true
        },
        "score": { "xp": 1800, "level": 3, "note": "diverges_from_balance" },
        "campaigns": [
          {
            "campaign_id": "aaaaaaaa-0000-0000-0000-000000000001",
            "slug": "your-campaign",
            "eligible_rewards": 18,
            "affordable_rewards": 7,
            "next_reachable_points": 2000,
            "redemptions": { "open": 1, "fulfilled": 3 }
          }
        ]
      }
    ],
    "total": 1,
    "found": 1
  },
  "meta": {
    "request_id": "1f6c1b2e-0000-4a1b-8c2d-000000000001",
    "timestamp": "2026-08-12T13:45:00.000Z"
  }
}
```

#### Campaign fields

| Field                                         | Meaning                                                                                                     |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `slug` · `name` · `tagline`                   | Identification and texts configured by your company                                                         |
| `currency.label`                              | **The currency name, as your company configured it.** Render this value — never hardcode a name in your app |
| `catalog.rewards`                             | Rewards that can be **redeemed with balance**                                                               |
| `catalog.awarded_only`                        | Rewards that are **granted** (by ranking or trigger) and are not for sale                                   |
| `catalog.categories`                          | Distinct categories in the catalog                                                                          |
| `catalog.cheapest_points` / `priciest_points` | Price range of the catalog                                                                                  |
| `terms`                                       | Present only when the campaign requires acceptance, with the current version                                |

#### Collaborator fields

| Field                               | Meaning                                                                              |
| ----------------------------------- | ------------------------------------------------------------------------------------ |
| `balance.spendable`                 | **The balance available to spend right now**                                         |
| `balance.wallet_enabled`            | Whether the wallet is enabled for your company — **read this before showing a zero** |
| `score.xp` · `score.level`          | Score and level. The level decides which rewards the collaborator qualifies for      |
| `score.note`                        | `diverges_from_balance` when score and balance do not match                          |
| `campaigns[].eligible_rewards`      | Rewards they **qualify for** (level rule satisfied)                                  |
| `campaigns[].affordable_rewards`    | Rewards they **can pay for** with the current balance                                |
| `campaigns[].next_reachable_points` | What the next out-of-reach reward costs — the "X to go"                              |
| `campaigns[].terms_accepted`        | Present only when the campaign requires acceptance                                   |
| `campaigns[].redemptions`           | Open and fulfilled redemptions                                                       |

<Warning>
  **`spendable: 0` does not mean "no points".** The wallet is enabled per company; until it is, the balance comes back as zero even for people who have a score. Always read `wallet_enabled` before writing "no balance" on the screen — otherwise it states an absence that isn't true.
</Warning>

<Tip>
  **Qualifying and affording are different counts.** A reward can be unlocked by level and still cost more than the collaborator has. That is why `eligible_rewards` and `affordable_rewards` are separate fields.
</Tip>

***

### Action: campaign\_catalog

The same content with the catalog cut. Because the catalog grows per collaborator, the **batch limit drops to 100**.

```bash theme={null}
curl -X POST https://api.play2sell.com/functions/v1/campaigns-partner-api \
  -H "Authorization: P2S-SIGN-V1 API_KEY:TIMESTAMP:SIGNATURE" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "campaign_catalog",
    "collaborators": [{ "cpf": "529.982.247-25" }],
    "campaign_slug": "your-campaign"
  }'
```

***

## Self mode (federated session)

With a federated user token, send `Authorization: Bearer <jwt>` and **omit** `collaborators`. The answer covers only the token's own user.

<Warning>
  Sending `collaborators` with a user token is rejected with `SELF_MODE_NO_COLLABORATORS`. Batch lookups require a partner API key — otherwise any signed-in user could read other people's balance by CPF.
</Warning>

***

## Error Handling

```json theme={null}
{ "error": { "code": "ERROR_CODE", "message": "Human-readable description", "details": [] } }
```

<Warning>
  **Two shapes, not one.** The block above is the *endpoint* error. Failures in the **authentication layer** answer before the endpoint runs, with `error` as a **string**:

  ```json theme={null}
  { "error": "Missing Authorization header", "code": "header_missing" }
  ```

  Code that assumes `error.code` reads `undefined` on every auth failure. Branch on the type — see [API Conventions](/api/conventions#error-shapes).
</Warning>

<AccordionGroup>
  <Accordion title="400 — VALIDATION_ERROR">
    Invalid body, batch over the limit, CPF without 11 digits, an item with neither `cpf` nor `email`, or a malformed `campaign_slug`. The `details` array points to the item `index`.
  </Accordion>

  <Accordion title="400 — SELF_MODE_NO_COLLABORATORS">
    A `collaborators` list was sent together with a user token.
  </Accordion>

  <Accordion title="401 — UNAUTHORIZED">
    API key is missing, invalid, or expired, or the signature does not match.
  </Accordion>

  <Accordion title="403 — FORBIDDEN">
    API key is valid but lacks the `campaigns:read` scope.
  </Accordion>

  <Accordion title="405 — METHOD_NOT_ALLOWED">
    Only `POST` is accepted.
  </Accordion>

  <Accordion title="429 — RATE_LIMITED">
    Too many requests this hour. Wait `retry_after` seconds.
  </Accordion>

  <Accordion title="500 — SERVER_ERROR">
    Internal server error. Retry with exponential backoff (2s, 4s, 8s).
  </Accordion>
</AccordionGroup>

<Tip>
  **No running campaign is not an error.** The response comes back with `campaigns: []` and status 200 — the company may simply have nothing live.
</Tip>

***

## Rate Limits

| Limit                                    | Value |
| ---------------------------------------- | ----- |
| Default requests per hour                | 1000  |
| Max collaborators per `campaign_status`  | 500   |
| Max collaborators per `campaign_catalog` | 100   |

***

## Security

* Read-only: this API never creates redemptions, accepts terms, or changes balances
* Each key is scoped to a single company — a CPF from another company answers `found: false`, and other companies' campaigns never appear
* Requests are HMAC-signed (P2S-SIGN-V1) and logged for audit; documents are never written to logs

<Warning>
  Never expose your API key in client-side code. This API must only be called from your backend server.
</Warning>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Missions API" icon="bullseye-arrow" href="/api/integrations/missions">
    The progress that generates the points spent here
  </Card>

  <Card title="Authentication" icon="key" href="/api/authentication">
    How to create and manage API Keys
  </Card>
</CardGroup>
