> This page is for version v2 (default).
> For other versions, use one of these documentation indexes:
> - v2 (default): https://docs.paid.ai/v-2/llms.txt
> - v1: https://docs.paid.ai/v-1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.paid.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paid.ai/_mcp/server.

# Send signals with a customer API key

> Give a customer's own runtime a key that reports usage for that customer and nothing else

When your agent runs inside a customer's own infrastructure, it still has to
send signals to Paid, but that environment should never hold your organization
API key. A customer API key is bound to one customer, and the only thing it can
do is send that customer's signals through
[`POST /api/v2/signals/bulk`](/api-reference/api-reference/signals/create-signals).

## Prerequisites

* An organization [API key](https://app.paid.ai/settings/api-keys) with the
  `write:api-keys` scope. Full-access keys have it.
* The customer, created in Paid. You address it by its Paid ID (`cus_...`) or
  by your own external ID.
* Customer API keys turned on for your organization. Ask your Paid contact to
  enable them; until then, creating one returns `403` `FEATURE_NOT_ENABLED`.

## Create a customer API key

Create the key from your own backend with your organization key, or from the
**API keys** tab on the customer's page in the dashboard.

```bash
curl -X POST https://api.agentpaid.io/api/v2/customers/external/customer_123/api-keys \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Tenant runtime (production)"}'
```

```json
{
  "id": "83d66fd2-bd47-4b42-8f3b-ff6c147de9e1",
  "name": "Tenant runtime (production)",
  "description": null,
  "customerId": "cus_abc123",
  "customerExternalId": "customer_123",
  "scopes": ["write:usage"],
  "keyPrefix": "paid_ck_live_rV8",
  "last4": "ky7C",
  "status": "ACTIVE",
  "createdAt": "2026-09-29T14:57:25.897Z",
  "lastUsedAt": null,
  "expiresAt": null,
  "revokedAt": null,
  "key": "paid_ck_live_rV8..."
}
```

The secret is in `key`, prefixed `paid_ck_live_`, or `paid_ck_test_` in a
sandbox organization. Paid returns it only this once and keeps just its hash,
so put it straight into the customer environment's secret store. Pass an
optional `expiresAt` to limit its life; without one, the key works until you
revoke it.

## Send signals

Use the customer key as the bearer token, exactly as you would an organization
key. Every signal must name the key's customer, by `customerId` or
`externalCustomerId`.

#### Node.js

```typescript
import { PaidClient } from "@paid-ai/paid-node";

const paid = new PaidClient({
  token: process.env.PAID_CUSTOMER_API_KEY ?? "YOUR_CUSTOMER_API_KEY",
});

async function main() {
  await paid.signals.createSignals({
    signals: [
      {
        eventName: "document_processed",
        customer: { externalCustomerId: "customer_123" },
        attribution: { externalProductId: "doc_processor" },
        data: { pages: 47 },
      },
    ],
  });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

#### Python

```python
import os

from paid import Paid, Signal, CustomerByExternalId, ProductByExternalId

paid = Paid(token=os.environ.get("PAID_CUSTOMER_API_KEY", "YOUR_CUSTOMER_API_KEY"))

paid.signals.create_signals(
    signals=[
        Signal(
            event_name="document_processed",
            customer=CustomerByExternalId(
                external_customer_id="customer_123",
            ),
            attribution=ProductByExternalId(
                external_product_id="doc_processor",
            ),
            data={"pages": 47},
        )
    ],
)
```

#### Go

```go
import (
    "context"
    "os"

    paid "github.com/paid-ai/paid-go"
    paidclient "github.com/paid-ai/paid-go/client"
    "github.com/paid-ai/paid-go/option"
)

func sendSignal(ctx context.Context) error {
    paidClient := paidclient.NewClient(
        option.WithToken(os.Getenv("PAID_CUSTOMER_API_KEY")),
    )

    _, err := paidClient.Signals.CreateSignals(ctx, &paid.BulkSignalsRequest{
        Signals: []*paid.Signal{
            {
                EventName: "document_processed",
                Customer: &paid.CustomerAttribution{
                    CustomerByExternalID: &paid.CustomerByExternalID{
                        ExternalCustomerID: "customer_123",
                    },
                },
                Attribution: &paid.Attribution{
                    ProductByExternalID: &paid.ProductByExternalID{
                        ExternalProductID: "doc_processor",
                    },
                },
                Data: map[string]interface{}{"pages": 47},
            },
        },
    })
    return err
}
```

## What the key can and cannot do

| Request                                                    | Result                                                                          |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Signals for the key's own customer                         | Accepted, the same as with an organization key                                  |
| A batch where any signal names another or unknown customer | The whole batch is refused with `403` `CUSTOMER_MISMATCH` and nothing is stored |
| Any other endpoint                                         | Refused with `403` `CUSTOMER_KEY_FORBIDDEN`                                     |
| A revoked or expired key                                   | Refused with `401`                                                              |

Idempotency keys sent with a customer key are scoped to that customer: before
checking for duplicates, Paid prefixes them with the reserved `paid:ck:`
prefix and an identifier for the customer. The same `idempotencyKey` from two
different customers' keys, or from your organization key, is not a duplicate
of the other. Keep the `paid:ck:` prefix out of the idempotency keys your
organization key sends, as those could match a customer key's.

## Revoke a key

Revoke a key with
[`DELETE /api/v2/customers/{id}/api-keys/{apiKeyId}`](/api-reference/api-reference/customers/revoke-customer-api-key),
its external ID twin, or the customer's **API keys** tab. It stops working
within a minute. Keys are never deleted: a revoked key stays in the list with
`revokedAt` set, and deleting a customer revokes its keys too.

## Related

* [First signals](/documentation/getting-started/first-signals)
* [Create a customer API key](/api-reference/api-reference/customers/create-customer-api-key)
* [List customer API keys](/api-reference/api-reference/customers/list-customer-api-keys)
* [Create signals in bulk](/api-reference/api-reference/signals/create-signals)