shopify-starter-kit 1.0.0
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/.agent/skills/shopify-apps/SKILL.md +47 -0
- package/.agent/skills/shopify-automation/SKILL.md +172 -0
- package/.agent/skills/shopify-development/README.md +60 -0
- package/.agent/skills/shopify-development/SKILL.md +368 -0
- package/.agent/skills/shopify-development/references/app-development.md +578 -0
- package/.agent/skills/shopify-development/references/extensions.md +555 -0
- package/.agent/skills/shopify-development/references/themes.md +498 -0
- package/.agent/skills/shopify-development/scripts/requirements.txt +19 -0
- package/.agent/skills/shopify-development/scripts/shopify_graphql.py +428 -0
- package/.agent/skills/shopify-development/scripts/shopify_init.py +441 -0
- package/.agent/skills/shopify-development/scripts/tests/test_shopify_init.py +379 -0
- package/bin/cli.js +3 -0
- package/package.json +32 -0
- package/src/index.js +116 -0
- package/templates/.agent/skills/shopify-apps/SKILL.md +47 -0
- package/templates/.agent/skills/shopify-automation/SKILL.md +172 -0
- package/templates/.agent/skills/shopify-development/README.md +60 -0
- package/templates/.agent/skills/shopify-development/SKILL.md +368 -0
- package/templates/.agent/skills/shopify-development/references/app-development.md +578 -0
- package/templates/.agent/skills/shopify-development/references/extensions.md +555 -0
- package/templates/.agent/skills/shopify-development/references/themes.md +498 -0
- package/templates/.agent/skills/shopify-development/scripts/requirements.txt +19 -0
- package/templates/.agent/skills/shopify-development/scripts/shopify_graphql.py +428 -0
- package/templates/.agent/skills/shopify-development/scripts/shopify_init.py +441 -0
- package/templates/.agent/skills/shopify-development/scripts/tests/test_shopify_init.py +379 -0
- package/templates/.devcontainer/devcontainer.json +27 -0
- package/templates/tests/playwright.config.ts +26 -0
- package/templates/tests/vitest.config.ts +9 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
# App Development Reference
|
|
2
|
+
|
|
3
|
+
Guide for building Shopify apps with OAuth, GraphQL/REST APIs, webhooks, and billing.
|
|
4
|
+
|
|
5
|
+
## OAuth Authentication
|
|
6
|
+
|
|
7
|
+
### OAuth 2.0 Flow
|
|
8
|
+
|
|
9
|
+
**1. Redirect to Authorization URL:**
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
https://{shop}.myshopify.com/admin/oauth/authorize?
|
|
13
|
+
client_id={api_key}&
|
|
14
|
+
scope={scopes}&
|
|
15
|
+
redirect_uri={redirect_uri}&
|
|
16
|
+
state={nonce}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**2. Handle Callback:**
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
app.get("/auth/callback", async (req, res) => {
|
|
23
|
+
const { code, shop, state } = req.query;
|
|
24
|
+
|
|
25
|
+
// Verify state to prevent CSRF
|
|
26
|
+
if (state !== storedState) {
|
|
27
|
+
return res.status(403).send("Invalid state");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Exchange code for access token
|
|
31
|
+
const accessToken = await exchangeCodeForToken(shop, code);
|
|
32
|
+
|
|
33
|
+
// Store token securely
|
|
34
|
+
await storeAccessToken(shop, accessToken);
|
|
35
|
+
|
|
36
|
+
res.redirect(`https://${shop}/admin/apps/${appHandle}`);
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**3. Exchange Code for Token:**
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
async function exchangeCodeForToken(shop, code) {
|
|
44
|
+
const response = await fetch(`https://${shop}/admin/oauth/access_token`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
client_id: process.env.SHOPIFY_API_KEY,
|
|
49
|
+
client_secret: process.env.SHOPIFY_API_SECRET,
|
|
50
|
+
code,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const { access_token } = await response.json();
|
|
55
|
+
return access_token;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Access Scopes
|
|
60
|
+
|
|
61
|
+
**Common Scopes:**
|
|
62
|
+
|
|
63
|
+
- `read_products`, `write_products` - Product catalog
|
|
64
|
+
- `read_orders`, `write_orders` - Order management
|
|
65
|
+
- `read_customers`, `write_customers` - Customer data
|
|
66
|
+
- `read_inventory`, `write_inventory` - Stock levels
|
|
67
|
+
- `read_fulfillments`, `write_fulfillments` - Order fulfillment
|
|
68
|
+
- `read_shipping`, `write_shipping` - Shipping rates
|
|
69
|
+
- `read_analytics` - Store analytics
|
|
70
|
+
- `read_checkouts`, `write_checkouts` - Checkout data
|
|
71
|
+
|
|
72
|
+
Full list: https://shopify.dev/api/usage/access-scopes
|
|
73
|
+
|
|
74
|
+
### Session Tokens (Embedded Apps)
|
|
75
|
+
|
|
76
|
+
For embedded apps using App Bridge:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
import { getSessionToken } from '@shopify/app-bridge/utilities';
|
|
80
|
+
|
|
81
|
+
async function authenticatedFetch(url, options = {}) {
|
|
82
|
+
const app = createApp({ ... });
|
|
83
|
+
const token = await getSessionToken(app);
|
|
84
|
+
|
|
85
|
+
return fetch(url, {
|
|
86
|
+
...options,
|
|
87
|
+
headers: {
|
|
88
|
+
...options.headers,
|
|
89
|
+
'Authorization': `Bearer ${token}`
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## GraphQL Admin API
|
|
96
|
+
|
|
97
|
+
### Making Requests
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
async function graphqlRequest(shop, accessToken, query, variables = {}) {
|
|
101
|
+
const response = await fetch(
|
|
102
|
+
`https://${shop}/admin/api/2026-01/graphql.json`,
|
|
103
|
+
{
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
"X-Shopify-Access-Token": accessToken,
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({ query, variables }),
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
|
|
115
|
+
if (data.errors) {
|
|
116
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return data.data;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Product Operations
|
|
124
|
+
|
|
125
|
+
**Create Product:**
|
|
126
|
+
|
|
127
|
+
```graphql
|
|
128
|
+
mutation CreateProduct($input: ProductInput!) {
|
|
129
|
+
productCreate(input: $input) {
|
|
130
|
+
product {
|
|
131
|
+
id
|
|
132
|
+
title
|
|
133
|
+
handle
|
|
134
|
+
}
|
|
135
|
+
userErrors {
|
|
136
|
+
field
|
|
137
|
+
message
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Variables:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"input": {
|
|
148
|
+
"title": "New Product",
|
|
149
|
+
"productType": "Apparel",
|
|
150
|
+
"vendor": "Brand",
|
|
151
|
+
"status": "ACTIVE",
|
|
152
|
+
"variants": [
|
|
153
|
+
{ "price": "29.99", "sku": "SKU-001", "inventoryQuantity": 100 }
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Update Product:**
|
|
160
|
+
|
|
161
|
+
```graphql
|
|
162
|
+
mutation UpdateProduct($input: ProductInput!) {
|
|
163
|
+
productUpdate(input: $input) {
|
|
164
|
+
product {
|
|
165
|
+
id
|
|
166
|
+
title
|
|
167
|
+
}
|
|
168
|
+
userErrors {
|
|
169
|
+
field
|
|
170
|
+
message
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Query Products:**
|
|
177
|
+
|
|
178
|
+
```graphql
|
|
179
|
+
query GetProducts($first: Int!, $query: String) {
|
|
180
|
+
products(first: $first, query: $query) {
|
|
181
|
+
edges {
|
|
182
|
+
node {
|
|
183
|
+
id
|
|
184
|
+
title
|
|
185
|
+
status
|
|
186
|
+
variants(first: 5) {
|
|
187
|
+
edges {
|
|
188
|
+
node {
|
|
189
|
+
id
|
|
190
|
+
price
|
|
191
|
+
inventoryQuantity
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
pageInfo {
|
|
198
|
+
hasNextPage
|
|
199
|
+
endCursor
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Order Operations
|
|
206
|
+
|
|
207
|
+
**Query Orders:**
|
|
208
|
+
|
|
209
|
+
```graphql
|
|
210
|
+
query GetOrders($first: Int!) {
|
|
211
|
+
orders(first: $first) {
|
|
212
|
+
edges {
|
|
213
|
+
node {
|
|
214
|
+
id
|
|
215
|
+
name
|
|
216
|
+
createdAt
|
|
217
|
+
displayFinancialStatus
|
|
218
|
+
totalPriceSet {
|
|
219
|
+
shopMoney {
|
|
220
|
+
amount
|
|
221
|
+
currencyCode
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
customer {
|
|
225
|
+
email
|
|
226
|
+
firstName
|
|
227
|
+
lastName
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Fulfill Order:**
|
|
236
|
+
|
|
237
|
+
```graphql
|
|
238
|
+
mutation FulfillOrder($fulfillment: FulfillmentInput!) {
|
|
239
|
+
fulfillmentCreate(fulfillment: $fulfillment) {
|
|
240
|
+
fulfillment {
|
|
241
|
+
id
|
|
242
|
+
status
|
|
243
|
+
trackingInfo {
|
|
244
|
+
number
|
|
245
|
+
url
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
userErrors {
|
|
249
|
+
field
|
|
250
|
+
message
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Webhooks
|
|
257
|
+
|
|
258
|
+
### Configuration
|
|
259
|
+
|
|
260
|
+
In `shopify.app.toml`:
|
|
261
|
+
|
|
262
|
+
```toml
|
|
263
|
+
[webhooks]
|
|
264
|
+
api_version = "2025-01"
|
|
265
|
+
|
|
266
|
+
[[webhooks.subscriptions]]
|
|
267
|
+
topics = ["orders/create"]
|
|
268
|
+
uri = "/webhooks/orders/create"
|
|
269
|
+
|
|
270
|
+
[[webhooks.subscriptions]]
|
|
271
|
+
topics = ["products/update"]
|
|
272
|
+
uri = "/webhooks/products/update"
|
|
273
|
+
|
|
274
|
+
[[webhooks.subscriptions]]
|
|
275
|
+
topics = ["app/uninstalled"]
|
|
276
|
+
uri = "/webhooks/app/uninstalled"
|
|
277
|
+
|
|
278
|
+
# GDPR mandatory webhooks
|
|
279
|
+
[webhooks.privacy_compliance]
|
|
280
|
+
customer_data_request_url = "/webhooks/gdpr/data-request"
|
|
281
|
+
customer_deletion_url = "/webhooks/gdpr/customer-deletion"
|
|
282
|
+
shop_deletion_url = "/webhooks/gdpr/shop-deletion"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Webhook Handler
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
import crypto from "crypto";
|
|
289
|
+
|
|
290
|
+
function verifyWebhook(req) {
|
|
291
|
+
const hmac = req.headers["x-shopify-hmac-sha256"];
|
|
292
|
+
const body = req.rawBody; // Raw body buffer
|
|
293
|
+
|
|
294
|
+
const hash = crypto
|
|
295
|
+
.createHmac("sha256", process.env.SHOPIFY_API_SECRET)
|
|
296
|
+
.update(body, "utf8")
|
|
297
|
+
.digest("base64");
|
|
298
|
+
|
|
299
|
+
return hmac === hash;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
app.post("/webhooks/orders/create", async (req, res) => {
|
|
303
|
+
if (!verifyWebhook(req)) {
|
|
304
|
+
return res.status(401).send("Unauthorized");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const order = req.body;
|
|
308
|
+
console.log("New order:", order.id, order.name);
|
|
309
|
+
|
|
310
|
+
// Process order...
|
|
311
|
+
|
|
312
|
+
res.status(200).send("OK");
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Common Webhook Topics
|
|
317
|
+
|
|
318
|
+
**Orders:**
|
|
319
|
+
|
|
320
|
+
- `orders/create`, `orders/updated`, `orders/delete`
|
|
321
|
+
- `orders/paid`, `orders/cancelled`, `orders/fulfilled`
|
|
322
|
+
|
|
323
|
+
**Products:**
|
|
324
|
+
|
|
325
|
+
- `products/create`, `products/update`, `products/delete`
|
|
326
|
+
|
|
327
|
+
**Customers:**
|
|
328
|
+
|
|
329
|
+
- `customers/create`, `customers/update`, `customers/delete`
|
|
330
|
+
|
|
331
|
+
**Inventory:**
|
|
332
|
+
|
|
333
|
+
- `inventory_levels/update`
|
|
334
|
+
|
|
335
|
+
**App:**
|
|
336
|
+
|
|
337
|
+
- `app/uninstalled` (critical for cleanup)
|
|
338
|
+
|
|
339
|
+
## Billing Integration
|
|
340
|
+
|
|
341
|
+
### App Charges
|
|
342
|
+
|
|
343
|
+
**One-time Charge:**
|
|
344
|
+
|
|
345
|
+
```graphql
|
|
346
|
+
mutation CreateCharge($input: AppPurchaseOneTimeInput!) {
|
|
347
|
+
appPurchaseOneTimeCreate(input: $input) {
|
|
348
|
+
appPurchaseOneTime {
|
|
349
|
+
id
|
|
350
|
+
name
|
|
351
|
+
price {
|
|
352
|
+
amount
|
|
353
|
+
}
|
|
354
|
+
status
|
|
355
|
+
confirmationUrl
|
|
356
|
+
}
|
|
357
|
+
userErrors {
|
|
358
|
+
field
|
|
359
|
+
message
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Variables:
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{
|
|
369
|
+
"input": {
|
|
370
|
+
"name": "Premium Feature",
|
|
371
|
+
"price": { "amount": 49.99, "currencyCode": "USD" },
|
|
372
|
+
"returnUrl": "https://your-app.com/billing/callback"
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Recurring Charge (Subscription):**
|
|
378
|
+
|
|
379
|
+
```graphql
|
|
380
|
+
mutation CreateSubscription(
|
|
381
|
+
$name: String!
|
|
382
|
+
$returnUrl: URL!
|
|
383
|
+
$lineItems: [AppSubscriptionLineItemInput!]!
|
|
384
|
+
$trialDays: Int
|
|
385
|
+
) {
|
|
386
|
+
appSubscriptionCreate(
|
|
387
|
+
name: $name
|
|
388
|
+
returnUrl: $returnUrl
|
|
389
|
+
lineItems: $lineItems
|
|
390
|
+
trialDays: $trialDays
|
|
391
|
+
) {
|
|
392
|
+
appSubscription {
|
|
393
|
+
id
|
|
394
|
+
name
|
|
395
|
+
status
|
|
396
|
+
}
|
|
397
|
+
confirmationUrl
|
|
398
|
+
userErrors {
|
|
399
|
+
field
|
|
400
|
+
message
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Variables:
|
|
407
|
+
|
|
408
|
+
```json
|
|
409
|
+
{
|
|
410
|
+
"name": "Monthly Subscription",
|
|
411
|
+
"returnUrl": "https://your-app.com/billing/callback",
|
|
412
|
+
"trialDays": 7,
|
|
413
|
+
"lineItems": [
|
|
414
|
+
{
|
|
415
|
+
"plan": {
|
|
416
|
+
"appRecurringPricingDetails": {
|
|
417
|
+
"price": { "amount": 29.99, "currencyCode": "USD" },
|
|
418
|
+
"interval": "EVERY_30_DAYS"
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Usage-based Billing:**
|
|
427
|
+
|
|
428
|
+
```graphql
|
|
429
|
+
mutation CreateUsageCharge(
|
|
430
|
+
$subscriptionLineItemId: ID!
|
|
431
|
+
$price: MoneyInput!
|
|
432
|
+
$description: String!
|
|
433
|
+
) {
|
|
434
|
+
appUsageRecordCreate(
|
|
435
|
+
subscriptionLineItemId: $subscriptionLineItemId
|
|
436
|
+
price: $price
|
|
437
|
+
description: $description
|
|
438
|
+
) {
|
|
439
|
+
appUsageRecord {
|
|
440
|
+
id
|
|
441
|
+
price {
|
|
442
|
+
amount
|
|
443
|
+
currencyCode
|
|
444
|
+
}
|
|
445
|
+
description
|
|
446
|
+
}
|
|
447
|
+
userErrors {
|
|
448
|
+
field
|
|
449
|
+
message
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Variables:
|
|
456
|
+
|
|
457
|
+
```json
|
|
458
|
+
{
|
|
459
|
+
"subscriptionLineItemId": "gid://shopify/AppSubscriptionLineItem/123",
|
|
460
|
+
"price": { "amount": "5.00", "currencyCode": "USD" },
|
|
461
|
+
"description": "100 API calls used"
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Metafields
|
|
466
|
+
|
|
467
|
+
### Create/Update Metafields
|
|
468
|
+
|
|
469
|
+
```graphql
|
|
470
|
+
mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
|
|
471
|
+
metafieldsSet(metafields: $metafields) {
|
|
472
|
+
metafields {
|
|
473
|
+
id
|
|
474
|
+
namespace
|
|
475
|
+
key
|
|
476
|
+
value
|
|
477
|
+
}
|
|
478
|
+
userErrors {
|
|
479
|
+
field
|
|
480
|
+
message
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Variables:
|
|
487
|
+
|
|
488
|
+
```json
|
|
489
|
+
{
|
|
490
|
+
"metafields": [
|
|
491
|
+
{
|
|
492
|
+
"ownerId": "gid://shopify/Product/123",
|
|
493
|
+
"namespace": "custom",
|
|
494
|
+
"key": "instructions",
|
|
495
|
+
"value": "Handle with care",
|
|
496
|
+
"type": "single_line_text_field"
|
|
497
|
+
}
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Metafield Types:**
|
|
503
|
+
|
|
504
|
+
- `single_line_text_field`, `multi_line_text_field`
|
|
505
|
+
- `number_integer`, `number_decimal`
|
|
506
|
+
- `date`, `date_time`
|
|
507
|
+
- `url`, `json`
|
|
508
|
+
- `file_reference`, `product_reference`
|
|
509
|
+
|
|
510
|
+
## Rate Limiting
|
|
511
|
+
|
|
512
|
+
### GraphQL Cost-Based Limits
|
|
513
|
+
|
|
514
|
+
**Limits:**
|
|
515
|
+
|
|
516
|
+
- Available points: 2000
|
|
517
|
+
- Restore rate: 100 points/second
|
|
518
|
+
- Max query cost: 2000
|
|
519
|
+
|
|
520
|
+
**Check Cost:**
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
const response = await graphqlRequest(shop, token, query);
|
|
524
|
+
const cost = response.extensions?.cost;
|
|
525
|
+
|
|
526
|
+
console.log(
|
|
527
|
+
`Cost: ${cost.actualQueryCost}/${cost.throttleStatus.maximumAvailable}`,
|
|
528
|
+
);
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Handle Throttling:**
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
async function graphqlWithRetry(shop, token, query, retries = 3) {
|
|
535
|
+
for (let i = 0; i < retries; i++) {
|
|
536
|
+
try {
|
|
537
|
+
return await graphqlRequest(shop, token, query);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (error.message.includes("Throttled") && i < retries - 1) {
|
|
540
|
+
await sleep(Math.pow(2, i) * 1000); // Exponential backoff
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Best Practices
|
|
550
|
+
|
|
551
|
+
**Security:**
|
|
552
|
+
|
|
553
|
+
- Store credentials in environment variables
|
|
554
|
+
- Verify webhook HMAC signatures
|
|
555
|
+
- Validate OAuth state parameter
|
|
556
|
+
- Use HTTPS for all endpoints
|
|
557
|
+
- Implement rate limiting on your endpoints
|
|
558
|
+
|
|
559
|
+
**Performance:**
|
|
560
|
+
|
|
561
|
+
- Cache access tokens securely
|
|
562
|
+
- Use bulk operations for large datasets
|
|
563
|
+
- Implement pagination for queries
|
|
564
|
+
- Monitor GraphQL query costs
|
|
565
|
+
|
|
566
|
+
**Reliability:**
|
|
567
|
+
|
|
568
|
+
- Implement exponential backoff for retries
|
|
569
|
+
- Handle webhook delivery failures
|
|
570
|
+
- Log errors for debugging
|
|
571
|
+
- Monitor app health metrics
|
|
572
|
+
|
|
573
|
+
**Compliance:**
|
|
574
|
+
|
|
575
|
+
- Implement GDPR webhooks (mandatory)
|
|
576
|
+
- Handle customer data deletion requests
|
|
577
|
+
- Provide data export functionality
|
|
578
|
+
- Follow data retention policies
|