Send signals with a customer API key

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.

Prerequisites

  • An organization API key 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.

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)"}'
{
"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.

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);
});

What the key can and cannot do

RequestResult
Signals for the key’s own customerAccepted, the same as with an organization key
A batch where any signal names another or unknown customerThe whole batch is refused with 403 CUSTOMER_MISMATCH and nothing is stored
Any other endpointRefused with 403 CUSTOMER_KEY_FORBIDDEN
A revoked or expired keyRefused 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}, 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.