zuplo 6.69.4 → 6.69.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/articles/api-key-administration.mdx +3 -3
- package/docs/articles/api-key-api.mdx +16 -6
- package/docs/articles/api-key-authentication.mdx +1 -1
- package/docs/articles/api-key-best-practices.mdx +199 -0
- package/docs/articles/api-key-buckets.mdx +1 -0
- package/docs/articles/api-key-end-users.mdx +83 -33
- package/docs/articles/api-key-management.mdx +34 -111
- package/docs/articles/api-key-react-component.mdx +19 -4
- package/docs/articles/api-key-self-serve-integration.mdx +441 -0
- package/docs/articles/api-key-service-limits.mdx +2 -1
- package/docs/articles/monetization/quickstart.md +9 -0
- package/docs/articles/monetization/stripe-integration.md +75 -0
- package/docs/articles/when-to-use-api-keys.md +191 -0
- package/docs/cli/info.mdx +37 -0
- package/docs/cli/project-info.mdx +37 -0
- package/docs/concepts/api-keys.md +45 -25
- package/docs/errors/unauthorized.mdx +2 -2
- package/docs/policies/monetization-inbound/schema.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Build Self-Serve Key Management
|
|
3
|
+
sidebar_label: Self-Serve Integration
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
If you want your users to create, view, rotate, and delete API keys from within
|
|
7
|
+
your own application rather than the Zuplo Developer Portal, you can build that
|
|
8
|
+
experience using the [Zuplo Developer API](https://dev.zuplo.com/docs/). This
|
|
9
|
+
guide walks through the architecture, the API operations you need, and the
|
|
10
|
+
security considerations for a production integration.
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
A self-serve integration has three parts:
|
|
15
|
+
|
|
16
|
+
1. **Your frontend** - the settings page or dashboard where users manage their
|
|
17
|
+
keys.
|
|
18
|
+
2. **Your backend** - a server-side proxy that authenticates the user with your
|
|
19
|
+
own auth system, then calls the Zuplo Developer API on their behalf.
|
|
20
|
+
3. **Zuplo Developer API** - the management API at `https://dev.zuplo.com` that
|
|
21
|
+
handles consumer and key CRUD operations.
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
The frontend calls API routes on your backend (for example, `/api/keys`), which
|
|
26
|
+
authenticate the user and proxy the request to Zuplo. The frontend never
|
|
27
|
+
communicates with the Zuplo Developer API directly.
|
|
28
|
+
|
|
29
|
+
:::caution
|
|
30
|
+
|
|
31
|
+
Never call the Zuplo Developer API directly from the browser. The API requires a
|
|
32
|
+
Zuplo API key (a `Bearer` token) that grants full management access to your
|
|
33
|
+
account's consumers and keys. Exposing it client-side would allow anyone to
|
|
34
|
+
create, delete, or read keys for any consumer.
|
|
35
|
+
|
|
36
|
+
:::
|
|
37
|
+
|
|
38
|
+
Your backend acts as the security boundary. It verifies the user's identity
|
|
39
|
+
using your own authentication (session cookie, JWT, etc.), determines which
|
|
40
|
+
Zuplo consumer they map to, and proxies only the operations they are authorized
|
|
41
|
+
to perform.
|
|
42
|
+
|
|
43
|
+
## Prerequisites
|
|
44
|
+
|
|
45
|
+
Before you start, you need:
|
|
46
|
+
|
|
47
|
+
- A Zuplo project with the
|
|
48
|
+
[API Key Authentication policy](../policies/api-key-inbound.mdx) configured on
|
|
49
|
+
your routes.
|
|
50
|
+
- A **Zuplo API key** for the Developer API. Create one in the Zuplo Portal
|
|
51
|
+
under **Settings > Zuplo API Keys**.
|
|
52
|
+
[More information](./accounts/zuplo-api-keys).
|
|
53
|
+
- Your **account name** and **bucket name**. A bucket groups consumers for an
|
|
54
|
+
environment - each project has buckets for production, preview, and
|
|
55
|
+
development. Find these in the Zuplo Portal under **Settings > General**.
|
|
56
|
+
- An application with server-side code and existing user authentication.
|
|
57
|
+
|
|
58
|
+
All examples in this guide use these environment variables:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Your Zuplo Account Name
|
|
62
|
+
export ZUPLO_ACCOUNT=my-account
|
|
63
|
+
# Your bucket name (found in Settings > General)
|
|
64
|
+
export ZUPLO_BUCKET=my-bucket
|
|
65
|
+
# Your Zuplo API Key (found in Settings > Zuplo API Keys)
|
|
66
|
+
export ZUPLO_API_KEY=zpka_YOUR_API_KEY
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Mapping users to consumers
|
|
70
|
+
|
|
71
|
+
A Zuplo **consumer** represents the identity behind one or more API keys. When a
|
|
72
|
+
user in your application needs API access, you create a consumer for them in
|
|
73
|
+
Zuplo.
|
|
74
|
+
|
|
75
|
+
The consumer's `name` must be unique within the bucket and is used as
|
|
76
|
+
`request.user.sub` when their key authenticates a request. A good pattern is to
|
|
77
|
+
use a stable identifier from your system, such as `org_123` or `user_456`.
|
|
78
|
+
|
|
79
|
+
Use **tags** to link the consumer back to your internal data. Tags are key-value
|
|
80
|
+
pairs that you can filter on when listing or mutating consumers. For example,
|
|
81
|
+
storing `orgId` as a tag lets you scope every API call to a specific
|
|
82
|
+
organization, which is critical for [multi-tenant security](#secure-with-tags).
|
|
83
|
+
|
|
84
|
+
Use **metadata** to store information that should be available at runtime when
|
|
85
|
+
the key is used. This populates `request.user.data` and is commonly used for
|
|
86
|
+
plan tiers, customer IDs, and feature flags.
|
|
87
|
+
|
|
88
|
+
## Automating consumer creation on signup
|
|
89
|
+
|
|
90
|
+
Rather than requiring users to manually request API access, create a Zuplo
|
|
91
|
+
consumer as part of your signup or onboarding flow. When a new organization or
|
|
92
|
+
user is created in your system, make a server-side call to create the consumer
|
|
93
|
+
with the appropriate metadata and tags.
|
|
94
|
+
|
|
95
|
+
Creating a consumer with a `name` that already exists returns a `409 Conflict`,
|
|
96
|
+
so your backend should catch this response for retry safety (for example,
|
|
97
|
+
treating 409 as a success if the consumer already belongs to the same user).
|
|
98
|
+
|
|
99
|
+
If a consumer does not exist yet and you attempt to list its keys, the API
|
|
100
|
+
returns a `404 Not Found`. Make sure your onboarding flow creates the consumer
|
|
101
|
+
before your frontend tries to fetch keys.
|
|
102
|
+
|
|
103
|
+
This is also the right place to sync billing information. For example, if a user
|
|
104
|
+
upgrades their plan, update the consumer's metadata so that downstream policies
|
|
105
|
+
and handlers see the new plan on the next authenticated request.
|
|
106
|
+
|
|
107
|
+
## Core operations
|
|
108
|
+
|
|
109
|
+
The following operations cover what most self-serve integrations need. Each
|
|
110
|
+
section shows the API call your backend should make.
|
|
111
|
+
|
|
112
|
+
:::note
|
|
113
|
+
|
|
114
|
+
Consumers and API keys are subject to service limits. See
|
|
115
|
+
[API Key Service Limits](./api-key-service-limits.mdx) for current maximums.
|
|
116
|
+
|
|
117
|
+
:::
|
|
118
|
+
|
|
119
|
+
### Create a consumer with an API key
|
|
120
|
+
|
|
121
|
+
When a user requests API access for the first time, create a consumer and an
|
|
122
|
+
initial API key in a single call by passing `?with-api-key=true`:
|
|
123
|
+
|
|
124
|
+
```shell
|
|
125
|
+
curl \
|
|
126
|
+
https://dev.zuplo.com/v1/accounts/$ZUPLO_ACCOUNT/key-buckets/$ZUPLO_BUCKET/consumers?with-api-key=true \
|
|
127
|
+
--request POST \
|
|
128
|
+
--header "Content-Type: application/json" \
|
|
129
|
+
--header "Authorization: Bearer $ZUPLO_API_KEY" \
|
|
130
|
+
--data @- << EOF
|
|
131
|
+
{
|
|
132
|
+
"name": "org_123",
|
|
133
|
+
"description": "Acme Corp",
|
|
134
|
+
"metadata": {
|
|
135
|
+
"plan": "growth",
|
|
136
|
+
"customerId": "cust_abc"
|
|
137
|
+
},
|
|
138
|
+
"tags": {
|
|
139
|
+
"orgId": "org_123"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
EOF
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The response includes the consumer and an `apiKeys` array with the generated
|
|
146
|
+
key:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"id": "csmr_sikZcE754kJu17X8yahPFO8J",
|
|
151
|
+
"name": "org_123",
|
|
152
|
+
"description": "Acme Corp",
|
|
153
|
+
"createdOn": "2026-04-16T10:00:00.000Z",
|
|
154
|
+
"updatedOn": "2026-04-16T10:00:00.000Z",
|
|
155
|
+
"tags": { "orgId": "org_123" },
|
|
156
|
+
"metadata": { "plan": "growth", "customerId": "cust_abc" },
|
|
157
|
+
"apiKeys": [
|
|
158
|
+
{
|
|
159
|
+
"id": "key_AM7eAiR0BiaXTam951XmC9kK",
|
|
160
|
+
"createdOn": "2026-04-16T10:00:00.000Z",
|
|
161
|
+
"updatedOn": "2026-04-16T10:00:00.000Z",
|
|
162
|
+
"expiresOn": null,
|
|
163
|
+
"key": "zpka_d67b7e241bb948758f415b79aa8exxxx_2efbxxxx"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
:::tip
|
|
170
|
+
|
|
171
|
+
In production, include tags on every consumer you create and pass `tag.*` query
|
|
172
|
+
parameters on every API call. This ensures proper ownership scoping. See
|
|
173
|
+
[Secure with tags](#secure-with-tags) below for details.
|
|
174
|
+
|
|
175
|
+
:::
|
|
176
|
+
|
|
177
|
+
Display the `key` value to the user in your UI. Although Zuplo keys are
|
|
178
|
+
retrievable, the standard UX pattern is to show the full key at creation time
|
|
179
|
+
and display it masked on subsequent views.
|
|
180
|
+
|
|
181
|
+
### List a consumer's API keys
|
|
182
|
+
|
|
183
|
+
To render an "API Keys" page in your settings, fetch the consumer's keys:
|
|
184
|
+
|
|
185
|
+
```shell
|
|
186
|
+
curl \
|
|
187
|
+
https://dev.zuplo.com/v1/accounts/$ZUPLO_ACCOUNT/key-buckets/$ZUPLO_BUCKET/consumers/org_123/keys?key-format=masked \
|
|
188
|
+
--header "Authorization: Bearer $ZUPLO_API_KEY"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `key-format` parameter controls how the key value appears in the response:
|
|
192
|
+
|
|
193
|
+
- `masked` - returns a partially redacted key (e.g.,
|
|
194
|
+
`zpka_d67b...xxxx_2efbxxxx`). Use this for the default list view.
|
|
195
|
+
- `visible` - returns the full key. Use this behind a "Reveal" button.
|
|
196
|
+
- `none` - omits the key value entirely. Use this when you only need key
|
|
197
|
+
metadata (ID, dates, expiration).
|
|
198
|
+
|
|
199
|
+
The response:
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"data": [
|
|
204
|
+
{
|
|
205
|
+
"id": "key_AM7eAiR0BiaXTam951XmC9kK",
|
|
206
|
+
"createdOn": "2026-04-16T10:00:00.000Z",
|
|
207
|
+
"updatedOn": "2026-04-16T10:00:00.000Z",
|
|
208
|
+
"expiresOn": null,
|
|
209
|
+
"key": "zpka_d67b...xxxx_2efbxxxx"
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Create an additional API key
|
|
216
|
+
|
|
217
|
+
If a consumer needs more than one active key (for example, separate keys for
|
|
218
|
+
staging and production), create a key directly:
|
|
219
|
+
|
|
220
|
+
```shell
|
|
221
|
+
curl \
|
|
222
|
+
https://dev.zuplo.com/v1/accounts/$ZUPLO_ACCOUNT/key-buckets/$ZUPLO_BUCKET/consumers/org_123/keys \
|
|
223
|
+
--request POST \
|
|
224
|
+
--header "Content-Type: application/json" \
|
|
225
|
+
--header "Authorization: Bearer $ZUPLO_API_KEY" \
|
|
226
|
+
--data '{"description": "Production key"}'
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Rotate a key
|
|
230
|
+
|
|
231
|
+
Key rotation creates a new key and sets an expiration on existing keys, giving
|
|
232
|
+
the user a transition period to switch over. Use the roll-key endpoint:
|
|
233
|
+
|
|
234
|
+
```shell
|
|
235
|
+
curl \
|
|
236
|
+
https://dev.zuplo.com/v1/accounts/$ZUPLO_ACCOUNT/key-buckets/$ZUPLO_BUCKET/consumers/org_123/roll-key \
|
|
237
|
+
--request POST \
|
|
238
|
+
--header "Content-Type: application/json" \
|
|
239
|
+
--header "Authorization: Bearer $ZUPLO_API_KEY" \
|
|
240
|
+
--data '{"expiresOn": "2026-04-19T00:00:00.000Z"}'
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
This sets `expiresOn` on **all** existing non-expired keys for that consumer and
|
|
244
|
+
creates a new key with no expiration. If `expiresOn` is set to a date in the
|
|
245
|
+
past, existing keys expire immediately - effectively an instant revocation with
|
|
246
|
+
a new replacement key. In your UI, surface the transition period clearly - for
|
|
247
|
+
example: "Your current key will remain active until April 19. Update your
|
|
248
|
+
integration to use the new key before then."
|
|
249
|
+
|
|
250
|
+
For guidance on choosing transition period lengths, see
|
|
251
|
+
[choosing a transition period](./api-key-api.mdx#choosing-a-transition-period).
|
|
252
|
+
|
|
253
|
+
### Delete a key
|
|
254
|
+
|
|
255
|
+
To let users revoke a specific key immediately:
|
|
256
|
+
|
|
257
|
+
```shell
|
|
258
|
+
curl \
|
|
259
|
+
https://dev.zuplo.com/v1/accounts/$ZUPLO_ACCOUNT/key-buckets/$ZUPLO_BUCKET/consumers/org_123/keys/key_AM7eAiR0BiaXTam951XmC9kK \
|
|
260
|
+
--request DELETE \
|
|
261
|
+
--header "Authorization: Bearer $ZUPLO_API_KEY"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
:::warning
|
|
265
|
+
|
|
266
|
+
Key deletion is immediate and irreversible. Any request using that key will
|
|
267
|
+
start receiving `401 Unauthorized` responses as soon as the edge cache expires
|
|
268
|
+
(within [`cacheTtlSeconds`](../policies/api-key-inbound.mdx), default 60
|
|
269
|
+
seconds). Surface a confirmation dialog in your UI before calling this endpoint.
|
|
270
|
+
|
|
271
|
+
:::
|
|
272
|
+
|
|
273
|
+
### Update consumer metadata
|
|
274
|
+
|
|
275
|
+
When a user's plan changes or you need to update the information available at
|
|
276
|
+
runtime, patch the consumer:
|
|
277
|
+
|
|
278
|
+
```shell
|
|
279
|
+
curl \
|
|
280
|
+
https://dev.zuplo.com/v1/accounts/$ZUPLO_ACCOUNT/key-buckets/$ZUPLO_BUCKET/consumers/org_123 \
|
|
281
|
+
--request PATCH \
|
|
282
|
+
--header "Content-Type: application/json" \
|
|
283
|
+
--header "Authorization: Bearer $ZUPLO_API_KEY" \
|
|
284
|
+
--data '{"metadata": {"plan": "enterprise", "customerId": "cust_abc"}}'
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The updated metadata is available on the next request that authenticates with
|
|
288
|
+
any of that consumer's keys (subject to cache TTL).
|
|
289
|
+
|
|
290
|
+
## Secure with tags
|
|
291
|
+
|
|
292
|
+
Tags are the primary mechanism for enforcing ownership in multi-tenant
|
|
293
|
+
integrations. Most Zuplo Developer API endpoints accept `tag.*` query parameters
|
|
294
|
+
that filter results and - critically - reject the request if the tag does not
|
|
295
|
+
match.
|
|
296
|
+
|
|
297
|
+
For example, if your backend knows the authenticated user belongs to `org_123`,
|
|
298
|
+
append `?tag.orgId=org_123` to every call:
|
|
299
|
+
|
|
300
|
+
```shell
|
|
301
|
+
# List only consumers belonging to org_123
|
|
302
|
+
curl \
|
|
303
|
+
"https://dev.zuplo.com/v1/accounts/$ZUPLO_ACCOUNT/key-buckets/$ZUPLO_BUCKET/consumers?tag.orgId=org_123&include-api-keys=true&key-format=masked" \
|
|
304
|
+
--header "Authorization: Bearer $ZUPLO_API_KEY"
|
|
305
|
+
|
|
306
|
+
# Delete a consumer - fails if the consumer doesn't have tag orgId=org_123
|
|
307
|
+
curl \
|
|
308
|
+
"https://dev.zuplo.com/v1/accounts/$ZUPLO_ACCOUNT/key-buckets/$ZUPLO_BUCKET/consumers/org_123?tag.orgId=org_123" \
|
|
309
|
+
--request DELETE \
|
|
310
|
+
--header "Authorization: Bearer $ZUPLO_API_KEY"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
This prevents one user from operating on another user's consumers, even if they
|
|
314
|
+
somehow obtain a valid consumer name. Your backend should always derive the tag
|
|
315
|
+
value from the authenticated session - never from the request body or query
|
|
316
|
+
string sent by the frontend.
|
|
317
|
+
|
|
318
|
+
## Backend implementation example
|
|
319
|
+
|
|
320
|
+
Here is a minimal Express.js example showing how to proxy key operations through
|
|
321
|
+
your backend. Adapt this pattern to your framework and language.
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
import express from "express";
|
|
325
|
+
|
|
326
|
+
const app = express();
|
|
327
|
+
app.use(express.json());
|
|
328
|
+
|
|
329
|
+
const ZUPLO_BASE = "https://dev.zuplo.com/v1/accounts";
|
|
330
|
+
const ZUPLO_ACCOUNT = process.env.ZUPLO_ACCOUNT;
|
|
331
|
+
const ZUPLO_BUCKET = process.env.ZUPLO_BUCKET;
|
|
332
|
+
const ZUPLO_API_KEY = process.env.ZUPLO_API_KEY;
|
|
333
|
+
|
|
334
|
+
// TODO: Replace with your real auth middleware
|
|
335
|
+
function getAuthenticatedOrg(req: express.Request): string | null {
|
|
336
|
+
// Example: extract the org ID from a verified JWT set by your auth middleware.
|
|
337
|
+
// In a real app, req.auth is populated by middleware like express-jwt or
|
|
338
|
+
// passport after verifying the token signature and expiration.
|
|
339
|
+
const auth = (req as any).auth;
|
|
340
|
+
return auth?.orgId ?? null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// List keys for the authenticated user's consumer
|
|
344
|
+
app.get("/api/keys", async (req, res) => {
|
|
345
|
+
const orgId = getAuthenticatedOrg(req);
|
|
346
|
+
if (!orgId) return res.status(401).json({ error: "Unauthorized" });
|
|
347
|
+
|
|
348
|
+
const response = await fetch(
|
|
349
|
+
`${ZUPLO_BASE}/${ZUPLO_ACCOUNT}/key-buckets/${ZUPLO_BUCKET}/consumers/${orgId}/keys?key-format=masked`,
|
|
350
|
+
{
|
|
351
|
+
headers: { Authorization: `Bearer ${ZUPLO_API_KEY}` },
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
res.status(response.status).json(await response.json());
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Create a new key for the authenticated user's consumer
|
|
359
|
+
app.post("/api/keys", async (req, res) => {
|
|
360
|
+
const orgId = getAuthenticatedOrg(req);
|
|
361
|
+
if (!orgId) return res.status(401).json({ error: "Unauthorized" });
|
|
362
|
+
|
|
363
|
+
const response = await fetch(
|
|
364
|
+
`${ZUPLO_BASE}/${ZUPLO_ACCOUNT}/key-buckets/${ZUPLO_BUCKET}/consumers/${orgId}/keys`,
|
|
365
|
+
{
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: {
|
|
368
|
+
"Content-Type": "application/json",
|
|
369
|
+
Authorization: `Bearer ${ZUPLO_API_KEY}`,
|
|
370
|
+
},
|
|
371
|
+
body: JSON.stringify({ description: req.body.description }),
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
res.status(response.status).json(await response.json());
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Rotate keys for the authenticated user's consumer
|
|
379
|
+
app.post("/api/keys/rotate", async (req, res) => {
|
|
380
|
+
const orgId = getAuthenticatedOrg(req);
|
|
381
|
+
if (!orgId) return res.status(401).json({ error: "Unauthorized" });
|
|
382
|
+
|
|
383
|
+
const response = await fetch(
|
|
384
|
+
`${ZUPLO_BASE}/${ZUPLO_ACCOUNT}/key-buckets/${ZUPLO_BUCKET}/consumers/${orgId}/roll-key?tag.orgId=${orgId}`,
|
|
385
|
+
{
|
|
386
|
+
method: "POST",
|
|
387
|
+
headers: {
|
|
388
|
+
"Content-Type": "application/json",
|
|
389
|
+
Authorization: `Bearer ${ZUPLO_API_KEY}`,
|
|
390
|
+
},
|
|
391
|
+
body: JSON.stringify({ expiresOn: req.body.expiresOn }),
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
res.status(response.status).json(await response.json());
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Delete a specific key
|
|
399
|
+
app.delete("/api/keys/:keyId", async (req, res) => {
|
|
400
|
+
const orgId = getAuthenticatedOrg(req);
|
|
401
|
+
if (!orgId) return res.status(401).json({ error: "Unauthorized" });
|
|
402
|
+
|
|
403
|
+
const response = await fetch(
|
|
404
|
+
`${ZUPLO_BASE}/${ZUPLO_ACCOUNT}/key-buckets/${ZUPLO_BUCKET}/consumers/${orgId}/keys/${req.params.keyId}?tag.orgId=${orgId}`,
|
|
405
|
+
{
|
|
406
|
+
method: "DELETE",
|
|
407
|
+
headers: { Authorization: `Bearer ${ZUPLO_API_KEY}` },
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
res.sendStatus(response.status);
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
:::note
|
|
416
|
+
|
|
417
|
+
This example omits error handling for brevity. In production, handle these key
|
|
418
|
+
error responses from the Zuplo Developer API: `404` (consumer or key not found),
|
|
419
|
+
`409` (consumer name already exists), and `429` (rate limited). See the
|
|
420
|
+
[Zuplo Developer API documentation](https://dev.zuplo.com/docs/) for full
|
|
421
|
+
details on error responses.
|
|
422
|
+
|
|
423
|
+
:::
|
|
424
|
+
|
|
425
|
+
## Integration options
|
|
426
|
+
|
|
427
|
+
Depending on how much control you need, there are several ways to integrate:
|
|
428
|
+
|
|
429
|
+
| Approach | Effort | Control | Best for |
|
|
430
|
+
| ------------------------------------------------- | ------ | ------- | --------------------------------- |
|
|
431
|
+
| [Zuplo Developer Portal](./api-key-end-users.mdx) | None | Low | Teams that don't need a custom UI |
|
|
432
|
+
| Custom UI with the Developer API (this guide) | Medium | Full | Any stack, full control over UX |
|
|
433
|
+
|
|
434
|
+
## Next steps
|
|
435
|
+
|
|
436
|
+
- [API Key API reference](./api-key-api.mdx) - additional API operations
|
|
437
|
+
including querying consumers by tags and bulk key creation.
|
|
438
|
+
- [Zuplo Developer API documentation](https://dev.zuplo.com/docs/) - full
|
|
439
|
+
endpoint reference for all consumer, key, bucket, and manager operations.
|
|
440
|
+
- [API Key Authentication policy](../policies/api-key-inbound.mdx) - configure
|
|
441
|
+
how keys are validated on your routes.
|
|
@@ -245,6 +245,15 @@ charges.
|
|
|
245
245
|
4. Click **Configure** on the Stripe card.
|
|
246
246
|
5. Enter a **Name** and paste your **Stripe API Key**, then click **Save**.
|
|
247
247
|
|
|
248
|
+
:::tip
|
|
249
|
+
|
|
250
|
+
For production, prefer a Stripe **restricted key** (`rk_live_*`) over a secret
|
|
251
|
+
key. See
|
|
252
|
+
[Using a Stripe restricted key](./stripe-integration.md#using-a-stripe-restricted-key)
|
|
253
|
+
for the exact permissions to enable.
|
|
254
|
+
|
|
255
|
+
:::
|
|
256
|
+
|
|
248
257
|
:::warning
|
|
249
258
|
|
|
250
259
|
Always use your Stripe **test** key (`sk_test_...`) in the **Working Copy**
|
|
@@ -66,6 +66,81 @@ mode don't transfer to live mode and vice versa.
|
|
|
66
66
|
|
|
67
67
|
:::
|
|
68
68
|
|
|
69
|
+
## Using a Stripe restricted key
|
|
70
|
+
|
|
71
|
+
Zuplo accepts both **secret keys** (`sk_test_*`, `sk_live_*`) and **restricted
|
|
72
|
+
keys** (`rk_test_*`, `rk_live_*`) when you connect Stripe. For production, use a
|
|
73
|
+
restricted key — it follows the principle of least privilege and limits the
|
|
74
|
+
blast radius if the credential is ever leaked.
|
|
75
|
+
|
|
76
|
+
A Monetization V3 restricted key needs the following eight permissions. Leave
|
|
77
|
+
every other permission set to **None**.
|
|
78
|
+
|
|
79
|
+
| Stripe permission | Level | Why Zuplo needs it |
|
|
80
|
+
| -------------------------------------------------- | ----- | ---------------------------------------------------------------------------------- |
|
|
81
|
+
| Connect → Accounts | Read | Verifies the key on install and reads basic account details (country, currency) |
|
|
82
|
+
| Core → Customers | Write | Creates and updates Stripe Customers when developers subscribe |
|
|
83
|
+
| Core → Payment Methods | Read | Displays saved cards in the customer portal |
|
|
84
|
+
| Checkout → Checkout Sessions | Write | Creates checkout sessions when developers add a payment method |
|
|
85
|
+
| Billing → Customer portal | Write | Creates customer-portal sessions for self-service plan management |
|
|
86
|
+
| Billing → Invoices | Write | Issues, finalizes, and pays invoices for metered usage (also covers Invoice items) |
|
|
87
|
+
| Tax → Tax Calculations and Transactions | Write | Calculates tax via Stripe Tax when tax collection is enabled |
|
|
88
|
+
| Webhook → Webhook Endpoints and Event Destinations | Write | Registers the webhook Zuplo uses to receive payment events |
|
|
89
|
+
|
|
90
|
+
Zuplo doesn't use Stripe Subscriptions, Products, Prices, Payment Intents, Setup
|
|
91
|
+
Intents, Refunds, or Stripe Billing Meters — leave all of those at **None**.
|
|
92
|
+
|
|
93
|
+
### Create the restricted key
|
|
94
|
+
|
|
95
|
+
1. In the Stripe Dashboard, go to **Developers → API keys → Create restricted
|
|
96
|
+
key**.
|
|
97
|
+
2. Name the key something recognizable, for example `Zuplo Monetization (test)`
|
|
98
|
+
or `Zuplo Monetization (production)`.
|
|
99
|
+
3. For each of the eight permissions above, set the level shown in the table.
|
|
100
|
+
4. Click **Create key**, copy the value (`rk_test_...` or `rk_live_...`), and
|
|
101
|
+
paste it into **Services → Monetization Service → Payment Provider** in your
|
|
102
|
+
Zuplo project.
|
|
103
|
+
|
|
104
|
+
:::caution
|
|
105
|
+
|
|
106
|
+
Use a **test** restricted key (`rk_test_*`) for preview and working-copy
|
|
107
|
+
environments, and a **live** restricted key (`rk_live_*`) for production. Zuplo
|
|
108
|
+
rejects a live key on a non-production environment and vice versa.
|
|
109
|
+
|
|
110
|
+
:::
|
|
111
|
+
|
|
112
|
+
### Troubleshoot permission errors
|
|
113
|
+
|
|
114
|
+
If you see an error like:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
The provided key 'rk_test_...' does not have the required permissions for this endpoint.
|
|
118
|
+
Having the 'rak_accounts_kyc_basic_read' permission would allow this request to continue.
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The key is missing one of the permissions in the table above. Stripe's internal
|
|
122
|
+
name in the error (`rak_<resource>_<action>`) maps to a row in the table. The
|
|
123
|
+
most common omissions:
|
|
124
|
+
|
|
125
|
+
- **`rak_accounts_kyc_basic_read`** → enable **Connect → Accounts** at **Read**.
|
|
126
|
+
- **`rak_tax_calculations_*`** → enable **Tax → Tax Calculations and
|
|
127
|
+
Transactions** at **Write**.
|
|
128
|
+
- **`rak_webhook_endpoints_*`** → enable **Webhook → Webhook Endpoints and Event
|
|
129
|
+
Destinations** at **Write**.
|
|
130
|
+
- **`rak_invoices_*`** → enable **Billing → Invoices** at **Write**.
|
|
131
|
+
|
|
132
|
+
Edit the key in the Stripe Dashboard, tick the missing permission, save, and
|
|
133
|
+
retry the connection in Zuplo.
|
|
134
|
+
|
|
135
|
+
### Rotate the key
|
|
136
|
+
|
|
137
|
+
You can replace the connected key from the same **Payment Provider** screen. The
|
|
138
|
+
new key must:
|
|
139
|
+
|
|
140
|
+
- Use the same prefix mode (test or live) as the existing key.
|
|
141
|
+
- Belong to the same Stripe account.
|
|
142
|
+
- Carry all eight permissions above.
|
|
143
|
+
|
|
69
144
|
## What Zuplo creates in Stripe
|
|
70
145
|
|
|
71
146
|
When you publish a plan, corresponding objects are created in Stripe
|