zuplo 6.69.3 → 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.
@@ -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
+ ![Self-serve API key architecture: user's browser calls your backend API which authenticates the user and proxies the request to the Zuplo Developer API at dev.zuplo.com](../../public/media/api-key-self-serve-integration/diagram-1.png)
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.
@@ -1,5 +1,6 @@
1
1
  ---
2
- title: Service Limits
2
+ title: API Key Service Limits
3
+ sidebar_label: Service Limits
3
4
  ---
4
5
 
5
6
  Zuplo's API Key Service can handle billions of requests and tokens. The service
@@ -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