vybekiit 0.7.32 → 0.7.34
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/dist/bin.js +303 -85
- package/dist/global-skills/aws-serverless/SKILL.md +168 -32
- package/dist/global-skills/aws-serverless/references/api-gateway.md +17 -0
- package/dist/global-skills/aws-serverless/references/architecture.md +9 -13
- package/dist/global-skills/aws-serverless/references/lambda.md +3 -3
- package/dist/global-skills/aws-serverless/references/orchestration.md +46 -20
- package/dist/global-skills/aws-serverless/references/production.md +1 -1
- package/dist/global-skills/eas-simulator/SKILL.md +15 -15
- package/dist/global-skills/eas-simulator/references/controllers.md +11 -7
- package/dist/global-skills/eas-simulator/references/run-your-app.md +8 -8
- package/dist/global-skills/eas-simulator/references/troubleshooting.md +10 -6
- package/dist/global-skills/email-best-practices/references/sending-reliability.md +17 -22
- package/dist/global-skills/extension-to-functions-codebase/SKILL.md +1 -0
- package/dist/global-skills/firebase-ai-logic-basics/SKILL.md +161 -76
- package/dist/global-skills/firebase-ai-logic-basics/references/flutter_setup.md +37 -0
- package/dist/global-skills/firebase-ai-logic-basics/references/ios_setup.md +97 -3
- package/dist/global-skills/firebase-ai-logic-basics/references/usage_patterns_android.md +88 -3
- package/dist/global-skills/firebase-ai-logic-basics/references/usage_patterns_web.md +91 -3
- package/dist/global-skills/firebase-app-hosting-basics/SKILL.md +5 -1
- package/dist/global-skills/firebase-auth-basics/SKILL.md +1 -0
- package/dist/global-skills/firebase-auth-basics/references/client_sdk_android.md +9 -9
- package/dist/global-skills/firebase-auth-basics/references/client_sdk_web.md +3 -3
- package/dist/global-skills/firebase-auth-basics/references/flutter_setup.md +22 -19
- package/dist/global-skills/firebase-auth-basics/references/security_rules.md +2 -2
- package/dist/global-skills/firebase-basics/SKILL.md +6 -2
- package/dist/global-skills/firebase-basics/references/android_setup.md +101 -37
- package/dist/global-skills/firebase-basics/references/ios_setup.md +57 -0
- package/dist/global-skills/firebase-basics/references/web_setup.md +52 -0
- package/dist/global-skills/firebase-crashlytics/SKILL.md +1 -0
- package/dist/global-skills/firebase-crashlytics/references/android_setup.md +84 -84
- package/dist/global-skills/firebase-crashlytics/references/ios_setup.md +1 -0
- package/dist/global-skills/firebase-data-connect/SKILL.md +1 -0
- package/dist/global-skills/firebase-data-connect/reference/config.md +8 -8
- package/dist/global-skills/firebase-data-connect/reference/realtime.md +4 -1
- package/dist/global-skills/firebase-data-connect/reference/sdk_android.md +33 -34
- package/dist/global-skills/firebase-data-connect/reference/sdk_web.md +8 -3
- package/dist/global-skills/firebase-data-connect/reference/security.md +13 -13
- package/dist/global-skills/firebase-firestore/SKILL.md +17 -7
- package/dist/global-skills/firebase-firestore/references/enterprise/android_sdk_usage.md +32 -32
- package/dist/global-skills/firebase-firestore/references/enterprise/indexes.md +2 -1
- package/dist/global-skills/firebase-firestore/references/standard/android_sdk_usage.md +14 -14
- package/dist/global-skills/firebase-firestore/references/standard/indexes.md +2 -1
- package/dist/global-skills/firebase-hosting-basics/SKILL.md +1 -0
- package/dist/global-skills/firebase-remote-config-basics/SKILL.md +2 -1
- package/dist/global-skills/firebase-remote-config-basics/references/android_setup.md +36 -37
- package/dist/global-skills/firebase-security-rules-auditor/SKILL.md +7 -6
- package/dist/global-skills/firestore-rules-creation/SKILL.md +22 -10
- package/dist/global-skills/manifest.json +2 -1
- package/dist/global-skills/neon/SKILL.md +31 -10
- package/dist/global-skills/neon-ai-gateway/SKILL.md +43 -14
- package/dist/global-skills/neon-functions/SKILL.md +12 -11
- package/dist/global-skills/neon-functions/references/sse.md +2 -2
- package/dist/global-skills/neon-object-storage/SKILL.md +5 -4
- package/dist/global-skills/neon-postgres/SKILL.md +5 -2
- package/dist/global-skills/neon-postgres/references/full-text-search.md +27 -17
- package/dist/global-skills/neon-postgres/references/hybrid-search.md +19 -15
- package/dist/global-skills/neon-postgres/references/lakebase-search-drizzle.md +99 -14
- package/dist/global-skills/neon-postgres/references/vector-search.md +93 -10
- package/dist/global-skills/neon-realtime/SKILL.md +207 -0
- package/dist/global-skills/neon-realtime/references/mutations.md +743 -0
- package/dist/global-skills/neon-realtime/references/queries.md +509 -0
- package/dist/global-skills/neon-realtime/references/reactivity.md +407 -0
- package/dist/global-skills/neon-realtime/references/ssr.md +695 -0
- package/dist/global-skills/neon-realtime/references/subscribing.md +485 -0
- package/dist/global-skills/resend/SKILL.md +7 -16
- package/dist/global-skills/resend/references/domains.md +1 -1
- package/dist/global-skills/resend/references/inboxes.md +349 -0
- package/dist/global-skills/resend/references/installation.md +3 -3
- package/dist/global-skills/resend/references/sending/best-practices.md +3 -3
- package/dist/global-skills/resend/references/sending/single-email-examples.md +3 -3
- package/dist/global-skills/resend/references/webhooks.md +9 -0
- package/dist/global-skills/stripe-best-practices/SKILL.md +18 -18
- package/dist/global-skills/stripe-best-practices/references/tax.md +26 -14
- package/dist/global-skills/use-railway/SKILL.md +6 -6
- package/dist/global-skills/use-railway/references/iac.md +2 -1
- package/dist/global-skills/use-railway/references/tracing.md +13 -5
- package/dist/global-skills/workers-best-practices/SKILL.md +1 -1
- package/dist/global-skills/wrangler/SKILL.md +3 -3
- package/dist/global-skills/xcode-project-setup/SKILL.md +1 -0
- package/package.json +8 -8
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# Inboxes
|
|
2
|
+
|
|
3
|
+
An inbox sends and receives email at one address on your domain. Received and sent emails group into threads. Each thread is in one folder and can carry labels. Drafts hold unsent emails, and agent settings tell an AI agent how to handle the threads of the inbox.
|
|
4
|
+
|
|
5
|
+
Inboxes need Node.js `resend` 6.33.0 or later, or Python `resend` 2.50.0 or later.
|
|
6
|
+
|
|
7
|
+
If an agent acts on the content of received emails, also install the `agent-email-inbox` skill. It covers the security levels for untrusted input.
|
|
8
|
+
|
|
9
|
+
## SDK Methods
|
|
10
|
+
|
|
11
|
+
| Operation | Node.js | Python |
|
|
12
|
+
|-----------|---------|--------|
|
|
13
|
+
| Create inbox | `resend.inboxes.create(params)` | `resend.Inboxes.create(params)` |
|
|
14
|
+
| List inboxes | `resend.inboxes.list(params?)` | `resend.Inboxes.list(params?)` |
|
|
15
|
+
| Get inbox | `resend.inboxes.get(idOrEmail)` | `resend.Inboxes.get(inbox_id)` |
|
|
16
|
+
| Update inbox | `resend.inboxes.update(id, params)` | `resend.Inboxes.update(params)`, `id` inside params |
|
|
17
|
+
| Delete inbox | `resend.inboxes.remove(id)` | `resend.Inboxes.remove(inbox_id)` |
|
|
18
|
+
| Get agent settings | `resend.inboxes.agent.get({ inboxId })` | `resend.Inboxes.Agent.get(inbox_id)` |
|
|
19
|
+
| Update agent settings | `resend.inboxes.agent.update({ inboxId, ...params })` | `resend.Inboxes.Agent.update(params)`, `inbox_id` inside params |
|
|
20
|
+
| List labels | `resend.inboxes.labels.list({ inboxId })` | `resend.Inboxes.Labels.list(inbox_id)` |
|
|
21
|
+
| Create label | `resend.inboxes.labels.create({ inboxId, ...params })` | `resend.Inboxes.Labels.create(params)`, `inbox_id` inside params |
|
|
22
|
+
| Update label | `resend.inboxes.labels.update({ inboxId, labelId, ...params })` | `resend.Inboxes.Labels.update(params)`, `inbox_id` and `id` inside params |
|
|
23
|
+
| Delete label | `resend.inboxes.labels.remove({ inboxId, labelId })` | `resend.Inboxes.Labels.remove(inbox_id, label_id)` |
|
|
24
|
+
| List threads | `resend.inboxes.threads.list({ inboxId, ...params })` | `resend.Inboxes.Threads.list(inbox_id, params?)` |
|
|
25
|
+
| Search threads | `resend.inboxes.threads.search({ inboxId, ...params })` | `resend.Inboxes.Threads.search(inbox_id, params?)` |
|
|
26
|
+
| Get thread | `resend.inboxes.threads.get({ inboxId, threadId })` | `resend.Inboxes.Threads.get(inbox_id, thread_id)` |
|
|
27
|
+
| Update thread | `resend.inboxes.threads.update({ inboxId, threadId, ...params })` | `resend.Inboxes.Threads.update(params)`, `inbox_id` and `id` inside params |
|
|
28
|
+
| Delete thread | `resend.inboxes.threads.remove({ inboxId, threadId })` | `resend.Inboxes.Threads.remove(inbox_id, thread_id)` |
|
|
29
|
+
| List thread emails | `resend.inboxes.threads.emails.list({ inboxId, threadId, ...params })` | `resend.Inboxes.Threads.Emails.list(inbox_id, thread_id, params?)` |
|
|
30
|
+
| Get thread email | `resend.inboxes.threads.emails.get({ inboxId, threadId, emailId })` | `resend.Inboxes.Threads.Emails.get(inbox_id, thread_id, email_id)` |
|
|
31
|
+
| Reply to thread email | `resend.inboxes.threads.emails.reply({ inboxId, threadId, emailId, ...params })` | `resend.Inboxes.Threads.Emails.reply(params)`, `inbox_id`, `thread_id` and `email_id` inside params |
|
|
32
|
+
| Forward thread email | `resend.inboxes.threads.emails.forward({ inboxId, threadId, emailId, ...params })` | `resend.Inboxes.Threads.Emails.forward(params)`, `inbox_id`, `thread_id` and `email_id` inside params |
|
|
33
|
+
| List drafts | `resend.inboxes.drafts.list({ inboxId, ...params })` | `resend.Inboxes.Drafts.list(inbox_id, params?)` |
|
|
34
|
+
| Create draft | `resend.inboxes.drafts.create({ inboxId, ...params })` | `resend.Inboxes.Drafts.create(params)`, `inbox_id` inside params |
|
|
35
|
+
| Get draft | `resend.inboxes.drafts.get({ inboxId, draftId })` | `resend.Inboxes.Drafts.get(inbox_id, draft_id)` |
|
|
36
|
+
| Update draft | `resend.inboxes.drafts.update({ inboxId, draftId, ...params })` | `resend.Inboxes.Drafts.update(params)`, `inbox_id` and `id` inside params |
|
|
37
|
+
| Delete draft | `resend.inboxes.drafts.remove({ inboxId, draftId })` | `resend.Inboxes.Drafts.remove(inbox_id, draft_id)` |
|
|
38
|
+
| Send draft | `resend.inboxes.drafts.send({ inboxId, draftId })` | `resend.Inboxes.Drafts.send(inbox_id, draft_id)` |
|
|
39
|
+
|
|
40
|
+
Use `.remove()`, not `.delete()`, in the Node.js SDK.
|
|
41
|
+
|
|
42
|
+
## Inboxes
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// Create an inbox on a domain of your team
|
|
46
|
+
const { data: created, error } = await resend.inboxes.create({
|
|
47
|
+
emailAddress: 'support@acme.com',
|
|
48
|
+
name: 'Customer Support',
|
|
49
|
+
fromName: 'Ada from Support',
|
|
50
|
+
});
|
|
51
|
+
// created: { object: 'inbox', id }
|
|
52
|
+
|
|
53
|
+
// List inboxes, newest first
|
|
54
|
+
const { data: inboxes } = await resend.inboxes.list({ limit: 20 });
|
|
55
|
+
// inboxes.data[i]: { id, name, email_address, from_name, unread, last_received, created_at, updated_at }
|
|
56
|
+
|
|
57
|
+
// Get an inbox by ID or by email address
|
|
58
|
+
const { data: inbox } = await resend.inboxes.get('support@acme.com');
|
|
59
|
+
// inbox: { object, id, name, email_address, domain_id, receiving_address, from_name, unread, drafts, last_received, created_at, updated_at }
|
|
60
|
+
|
|
61
|
+
// Change the internal name or the name recipients see
|
|
62
|
+
const { data: updated } = await resend.inboxes.update('b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1', {
|
|
63
|
+
name: 'Support',
|
|
64
|
+
fromName: 'Acme Support',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Delete an inbox
|
|
68
|
+
const { data: deleted } = await resend.inboxes.remove('b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Key gotchas:**
|
|
72
|
+
- Create returns only `{ object, id }`. Call `get` for the other fields.
|
|
73
|
+
- Without `forwarding`, the domain must have receiving enabled, DKIM and SPF verified, and the inbound MX record verified or pending. With `forwarding: true`, the domain only needs DKIM and SPF, and Resend returns a `receiving_address` to forward mail to.
|
|
74
|
+
- Create returns 422 for a domain that does not meet these rules, 409 for an address already in use, 403 when the team is at the inbox limit of its plan, and 451 for a blocked account.
|
|
75
|
+
- `get` accepts the inbox ID or its email address, without case. Every other inbox endpoint accepts only the inbox ID.
|
|
76
|
+
- List items carry fewer fields than `get`.
|
|
77
|
+
- `fromName` is a plain name, not `Name <email>`. It cannot contain a colon, `<`, `>`, or line breaks. An empty string removes it.
|
|
78
|
+
- Update needs at least one of `name` or `fromName`.
|
|
79
|
+
|
|
80
|
+
## Agent Settings
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const inboxId = 'b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1';
|
|
84
|
+
|
|
85
|
+
const { data: agent } = await resend.inboxes.agent.get({ inboxId });
|
|
86
|
+
// agent: { object: 'inbox_agent', instructions, tone, actions }
|
|
87
|
+
|
|
88
|
+
const { data: updated } = await resend.inboxes.agent.update({
|
|
89
|
+
inboxId,
|
|
90
|
+
instructions: 'Answer refund questions yourself. Escalate legal threats.',
|
|
91
|
+
tone: 'concise_professional',
|
|
92
|
+
actions: ['draft_reply', 'add_labels'],
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Key gotchas:**
|
|
97
|
+
- An inbox without an agent returns `instructions: null`, `tone: null` and `actions: []`. Update creates the agent when the inbox has none.
|
|
98
|
+
- Omitted fields keep their value. Send `null` to clear `instructions` or `tone`.
|
|
99
|
+
- `actions` replaces the whole set, without duplicates. Send `[]` to disable all actions. The values are `draft_reply`, `forward_thread`, `add_labels`, `assign_thread`, `mark_as_spam`, `archive_thread` and `delete_thread`.
|
|
100
|
+
- `instructions` is at most 4000 characters.
|
|
101
|
+
- `tone` is one of `concise_professional`, `concise_casual`, `expanded_professional` and `expanded_casual`.
|
|
102
|
+
|
|
103
|
+
## Labels
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const inboxId = 'b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1';
|
|
107
|
+
|
|
108
|
+
const { data: label } = await resend.inboxes.labels.create({
|
|
109
|
+
inboxId,
|
|
110
|
+
name: 'Urgent',
|
|
111
|
+
color: '#E93D82',
|
|
112
|
+
});
|
|
113
|
+
// label: { object: 'inbox_label', id }
|
|
114
|
+
|
|
115
|
+
const { data: labels } = await resend.inboxes.labels.list({ inboxId });
|
|
116
|
+
// labels.data[i]: { id, name, color, created_at }
|
|
117
|
+
|
|
118
|
+
await resend.inboxes.labels.update({
|
|
119
|
+
inboxId,
|
|
120
|
+
labelId: label.id,
|
|
121
|
+
name: 'Billing',
|
|
122
|
+
color: '#F76B15',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await resend.inboxes.labels.remove({ inboxId, labelId: label.id });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Key gotchas:**
|
|
129
|
+
- `color` is a `#RRGGBB` hex string. The API returns it in uppercase. Without `color`, the API picks a random color.
|
|
130
|
+
- An inbox has at most 100 labels. Names are unique per inbox, compared without case. A duplicate name or a full inbox returns 422.
|
|
131
|
+
- The list is not paginated. It returns every label, oldest first.
|
|
132
|
+
- To label a thread, call `threads.update` with `addLabelIds`. `removeLabelIds` takes it off the thread and keeps the label. Only `labels.remove` deletes it.
|
|
133
|
+
- Deleting a label removes it from every thread. It does not send `inbox.thread.labels.updated` for those threads.
|
|
134
|
+
|
|
135
|
+
## Threads
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const inboxId = 'b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1';
|
|
139
|
+
const threadId = '7c1f0a2e-5d3b-4e8a-9f61-2b8d4c6e1a90';
|
|
140
|
+
const labelId = '7f9c1d2e-4a6b-4c3d-8e15-9b0a7c6d5e34';
|
|
141
|
+
|
|
142
|
+
// Unread threads in the Inbox folder, most recently active first
|
|
143
|
+
const { data: threads } = await resend.inboxes.threads.list({
|
|
144
|
+
inboxId,
|
|
145
|
+
folders: ['inbox'],
|
|
146
|
+
read: false,
|
|
147
|
+
limit: 20,
|
|
148
|
+
});
|
|
149
|
+
// threads.data[i]: { id, subject, from, to, cc, bcc, labels, email_count, has_attachment, has_draft, read, received_at, folder }
|
|
150
|
+
|
|
151
|
+
// Find threads by text, sender, recipient, attachment or date
|
|
152
|
+
const { data: results } = await resend.inboxes.threads.search({
|
|
153
|
+
inboxId,
|
|
154
|
+
query: 'invoice -draft',
|
|
155
|
+
from: ['billing@example.com'],
|
|
156
|
+
hasAttachment: true,
|
|
157
|
+
startDate: '2026-09-01',
|
|
158
|
+
endDate: '2026-09-30',
|
|
159
|
+
});
|
|
160
|
+
// results.data[i]: the thread fields plus matched_email_id and highlights
|
|
161
|
+
|
|
162
|
+
const { data: thread } = await resend.inboxes.threads.get({ inboxId, threadId });
|
|
163
|
+
// thread: { object, id, subject, folder, labels, read }
|
|
164
|
+
|
|
165
|
+
// Mark read, move to a folder, add and remove labels
|
|
166
|
+
const { data: changed } = await resend.inboxes.threads.update({
|
|
167
|
+
inboxId,
|
|
168
|
+
threadId,
|
|
169
|
+
read: true,
|
|
170
|
+
folder: 'archive',
|
|
171
|
+
addLabelIds: [labelId],
|
|
172
|
+
});
|
|
173
|
+
await resend.inboxes.threads.update({ inboxId, threadId, removeLabelIds: [labelId] });
|
|
174
|
+
|
|
175
|
+
// Move the thread to trash
|
|
176
|
+
const { data: trashed } = await resend.inboxes.threads.remove({ inboxId, threadId });
|
|
177
|
+
// trashed: { object, id, deleted }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Key gotchas:**
|
|
181
|
+
- Folders are `inbox`, `archive`, `spam`, `sent` and `trash`. A thread is in exactly one folder. The API sets `sent` for threads with only outbound emails. You cannot set it.
|
|
182
|
+
- `folders` defaults to `['inbox']`, or to `['inbox', 'archive', 'sent']` when `labels` is set. `spam` and `trash` are only included when you name them.
|
|
183
|
+
- `after` and `before` take a thread ID, not a date. Pass only one of them. A cursor thread that is not in the selected folders returns 404.
|
|
184
|
+
- Search can be up to about a minute behind new emails. All filters must match. `query`, `from`, `to`, `cc`, `bcc`, `hasAttachment`, `startDate` and `endDate` must all match the same email in the thread. When you set any of them, that email must itself be in one of `folders`.
|
|
185
|
+
- In `query`, every word must match. Wrap words in double quotes for an exact phrase. Start a word with `-` to exclude it.
|
|
186
|
+
- A search result has `matched_email_id`, the email that matched, and `highlights`, the matched text with each match in `**`. The thread email endpoints accept `matched_email_id` as `emailId`.
|
|
187
|
+
- Update needs at least one of `read`, `folder`, `addLabelIds` or `removeLabelIds`. Each label array takes 1 to 10 IDs. A label ID cannot be in both arrays, or the request returns 422.
|
|
188
|
+
- `removeLabelIds` takes the label off the thread. The label itself stays on the inbox.
|
|
189
|
+
- Update applies `read`, `folder`, added labels, then removed labels. It is not atomic. A 404 for a label leaves the earlier changes applied.
|
|
190
|
+
- `remove` moves the thread to `trash` and deletes it after 30 days. To restore it, update `folder` to `inbox`, `archive` or `spam`.
|
|
191
|
+
|
|
192
|
+
## Thread Emails
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const inboxId = 'b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1';
|
|
196
|
+
const threadId = '7c1f0a2e-5d3b-4e8a-9f61-2b8d4c6e1a90';
|
|
197
|
+
|
|
198
|
+
// Emails in the thread, oldest first
|
|
199
|
+
const { data: emails } = await resend.inboxes.threads.emails.list({ inboxId, threadId });
|
|
200
|
+
const emailId = emails.data[emails.data.length - 1].id;
|
|
201
|
+
|
|
202
|
+
// The full email, body included
|
|
203
|
+
const { data: email } = await resend.inboxes.threads.emails.get({ inboxId, threadId, emailId });
|
|
204
|
+
// email: { id, direction, from, to, cc, bcc, reply_to, subject, message_id, html, text, attachments, read, received_at }
|
|
205
|
+
|
|
206
|
+
// Reply from the inbox address. The API sets `to` from the email you reply to.
|
|
207
|
+
const { data: reply } = await resend.inboxes.threads.emails.reply({
|
|
208
|
+
inboxId,
|
|
209
|
+
threadId,
|
|
210
|
+
emailId,
|
|
211
|
+
text: 'Refund issued for order 1041.',
|
|
212
|
+
headers: { 'X-Ticket-Id': '4821' },
|
|
213
|
+
attachments: [
|
|
214
|
+
{ filename: 'receipt.txt', content: Buffer.from('Order 1041 refunded').toString('base64') },
|
|
215
|
+
],
|
|
216
|
+
}, { idempotencyKey: `refund-reply/${emailId}` });
|
|
217
|
+
// reply: { id } (the sent email ID)
|
|
218
|
+
|
|
219
|
+
// Forward the email with its attachments
|
|
220
|
+
const { data: forwarded } = await resend.inboxes.threads.emails.forward({
|
|
221
|
+
inboxId,
|
|
222
|
+
threadId,
|
|
223
|
+
emailId,
|
|
224
|
+
to: ['delivered@resend.dev'],
|
|
225
|
+
text: 'Flagging this refund request for you.',
|
|
226
|
+
}, { idempotencyKey: `refund-forward/${emailId}` });
|
|
227
|
+
// forwarded: { id } (the sent email ID)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Key gotchas:**
|
|
231
|
+
- `threads.emails.list` returns the emails oldest first. Pass the last `id` as `after` for the next, newer page. A cursor that is not an email of the thread returns 404.
|
|
232
|
+
- The `emailId` comes from `threads.emails.list` or from `matched_email_id` in a search result, not from `resend.emails`.
|
|
233
|
+
- Reply sends right away, with no stored draft. For a received email, the reply goes to its `Reply-To` addresses, or to its sender. For an email the inbox sent, it goes to that email's `to` recipients. You cannot set `to` on a reply.
|
|
234
|
+
- With `replyAll: true`, a reply to a received email also copies the other `To` addresses and the `cc` of that email. A reply-all to an email the inbox sent goes to its Reply-To when that is not the inbox address, else to its `to`, and copies its `cc`. The inbox address is left out, and duplicates are removed.
|
|
235
|
+
- Reply needs `html` or `text` with more than whitespace. Without `subject`, the thread subject is used with a `Re:` prefix. Forward uses a `Fwd:` prefix.
|
|
236
|
+
- Forward needs `to`. It quotes the original email and sends its attachments with your `attachments`. Together they can have up to 10 files and 25 MB of `content`. An original attachment that was sent from a URL (`path`) cannot be forwarded, and the request returns 422.
|
|
237
|
+
- `attachments` and `headers` have the same shape as on `resend.emails.send()`. Resend sets `In-Reply-To`, `References` and `Reply-To`. Setting one of them in `headers`, in any letter case, returns 422. An empty header name, or a line break or null character in a name or value, also returns 422.
|
|
238
|
+
- An empty file, a blocked file type, or a `path` that is not an `http` or `https` URL, or that points to `localhost`, returns 422.
|
|
239
|
+
- Reply and forward return only the sent email ID.
|
|
240
|
+
- Reply and forward take an `idempotencyKey` option, as on `resend.emails.send()`. See [Idempotency Keys](#idempotency-keys).
|
|
241
|
+
|
|
242
|
+
## Drafts
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
const inboxId = 'b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1';
|
|
246
|
+
|
|
247
|
+
// A draft that starts a new thread
|
|
248
|
+
const { data: draft } = await resend.inboxes.drafts.create({
|
|
249
|
+
inboxId,
|
|
250
|
+
to: ['delivered@resend.dev'],
|
|
251
|
+
subject: 'Refund for order 1041',
|
|
252
|
+
text: 'Refund issued for order 1041.',
|
|
253
|
+
attachments: [
|
|
254
|
+
{
|
|
255
|
+
filename: 'receipt.txt',
|
|
256
|
+
content: Buffer.from('Order 1041 refunded').toString('base64'),
|
|
257
|
+
contentType: 'text/plain',
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
}, { idempotencyKey: 'refund-draft/order-1041' });
|
|
261
|
+
// draft: { object: 'inbox_draft', id }
|
|
262
|
+
|
|
263
|
+
const { data: drafts } = await resend.inboxes.drafts.list({ inboxId, limit: 20 });
|
|
264
|
+
|
|
265
|
+
const { data: saved } = await resend.inboxes.drafts.get({ inboxId, draftId: draft.id });
|
|
266
|
+
// saved: { object, id, type, to, cc, bcc, subject, html, text, thread_id, reply_to_email_id, email_id, created_at, updated_at, attachments }
|
|
267
|
+
// saved.attachments[i]: { id, filename, content_type, size }
|
|
268
|
+
|
|
269
|
+
// Replace the body and every attachment
|
|
270
|
+
await resend.inboxes.drafts.update({
|
|
271
|
+
inboxId,
|
|
272
|
+
draftId: draft.id,
|
|
273
|
+
html: '<p>Refund issued for order 1041.</p>',
|
|
274
|
+
text: 'Refund issued for order 1041.',
|
|
275
|
+
attachments: [
|
|
276
|
+
{
|
|
277
|
+
filename: 'invoice-1041.txt',
|
|
278
|
+
content: Buffer.from('Invoice 1041, refunded in full').toString('base64'),
|
|
279
|
+
contentType: 'text/plain',
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const { data: sent } = await resend.inboxes.drafts.send(
|
|
285
|
+
{ inboxId, draftId: draft.id, headers: { 'X-Ticket-Id': '4821' } },
|
|
286
|
+
{ idempotencyKey: `refund-draft-send/${draft.id}` },
|
|
287
|
+
);
|
|
288
|
+
// sent: { id } (the sent email ID)
|
|
289
|
+
|
|
290
|
+
// Discard a draft you do not want to send
|
|
291
|
+
const { data: other } = await resend.inboxes.drafts.create({ inboxId, subject: 'Notes' });
|
|
292
|
+
await resend.inboxes.drafts.update({ inboxId, draftId: other.id, attachments: [] });
|
|
293
|
+
await resend.inboxes.drafts.remove({ inboxId, draftId: other.id });
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Key gotchas:**
|
|
297
|
+
- For a reply draft, pass `threadId` and `replyToEmailId` together. Without `to`, the API addresses it like a reply. Send `to: []` to leave it empty.
|
|
298
|
+
- Each email can have one unsent reply draft. Creating another returns 200 with the ID of the existing draft, not 201. That request ignores every other field except `attachments`, which replaces the attachments of the existing draft.
|
|
299
|
+
- A draft needs at least one non-empty `to`, `cc`, `bcc`, `subject`, `text`, `html` or `attachments`. `to`, `cc` and `bcc` together have at most 50 recipients.
|
|
300
|
+
- The draft stores its attachments, so each one needs `content` (base64), `filename` and `contentType`. A `path` or `contentId` returns 422. The files on a draft can total 25 MB, with no limit on the count. An empty file or a blocked file type, such as `.exe`, returns 422.
|
|
301
|
+
- An update with `attachments` replaces every attachment on the draft. Send `[]` to remove them all. Omit the field to keep them.
|
|
302
|
+
- An update that sets `text` or `html` replaces the whole body. The part you omit becomes `null`, so send both to keep both.
|
|
303
|
+
- Send needs at least one `to` recipient, a non-empty `subject`, and `html` or `text`, or it returns 422. A reply draft created without `subject` has an empty subject with no `Re:` prefix, so set one before you send it. Update, remove or send on a draft that was already sent returns 409.
|
|
304
|
+
- `drafts.list` returns only unsent drafts, most recently updated first.
|
|
305
|
+
- `drafts.send` takes `headers`, an object of string values, as on `resend.emails.send()`. Create and update do not take headers. The header rules of reply and forward apply.
|
|
306
|
+
- Create and send take an `idempotencyKey` option. See [Idempotency Keys](#idempotency-keys).
|
|
307
|
+
|
|
308
|
+
## Idempotency Keys
|
|
309
|
+
|
|
310
|
+
Create draft, send draft, reply and forward accept an idempotency key, the same as `resend.emails.send()`. Pass it in the options argument: `{ idempotencyKey }` in Node.js, `{"idempotency_key": ...}` in Python. Use it when you retry these calls, so a retry does not create a second draft or send a second email.
|
|
311
|
+
|
|
312
|
+
| Key Facts | |
|
|
313
|
+
|-----------|---|
|
|
314
|
+
| **Operations** | `drafts.create`, `drafts.send`, `threads.emails.reply`, `threads.emails.forward`. No other inbox operation takes it |
|
|
315
|
+
| **Format** | `<event-type>/<entity-id>` (e.g., `refund-reply/<email-id>`) |
|
|
316
|
+
| **Length** | 1 to 256 characters |
|
|
317
|
+
| **Expiration** | 24 hours |
|
|
318
|
+
| **Same key + same payload** | Returns the first successful (2xx) response without a new draft or email. The response has the `idempotent-replayed: true` header |
|
|
319
|
+
| **Failed first request** | Not stored. Retry it with the same key |
|
|
320
|
+
| **Same key + different payload** | Returns 409 `invalid_idempotent_request` |
|
|
321
|
+
| **Same key while the first request runs** | Returns 409 `concurrent_idempotent_requests` |
|
|
322
|
+
|
|
323
|
+
## Webhook Events
|
|
324
|
+
|
|
325
|
+
Inboxes send `inbox.*` events. See the event list in [webhooks.md](webhooks.md#other-events).
|
|
326
|
+
|
|
327
|
+
- Each payload `data` has `source` (`api`, `dashboard`, `agent` or `system`) and `inbox_id`, plus `thread_id`, `email_id` or `draft_id` where they apply.
|
|
328
|
+
- Email and draft payloads carry the object without `html` or `text`. Call `threads.emails.get()` or `drafts.get()` for the body.
|
|
329
|
+
- The `draft` object in `inbox.draft.*` payloads lists `attachments` as `{ id, filename, content_type, size }`, without the file content.
|
|
330
|
+
- `inbox.deleted` carries only `inbox_id`. `inbox.draft.deleted` has no `draft` object.
|
|
331
|
+
- `inbox.thread.labels.updated` carries one label, in `added` or `removed`.
|
|
332
|
+
- Sending a draft sends both `inbox.draft.sent` and `inbox.email.sent`, in no fixed order.
|
|
333
|
+
|
|
334
|
+
## Common Mistakes
|
|
335
|
+
|
|
336
|
+
| Mistake | Fix |
|
|
337
|
+
|---------|-----|
|
|
338
|
+
| Using an email address on `update`, `remove` or a sub-resource | Only `inboxes.get` accepts an address. Use the inbox ID everywhere else |
|
|
339
|
+
| Reading fields from the create response | Create inbox, label and draft return only `{ object, id }`. Call `get` |
|
|
340
|
+
| Setting `Reply-To` in `headers` on a reply, forward or draft send | Resend sets `In-Reply-To`, `References` and `Reply-To`. Leave them out of `headers` |
|
|
341
|
+
| Passing `to` on a reply | Reply sets `to` from the email you reply to. Use `forward` to send to someone new |
|
|
342
|
+
| Using an ID from `resend.emails.list()` as `emailId` | Use the ID from `threads.emails.list` or `matched_email_id` from a search |
|
|
343
|
+
| Expecting `threads.remove` to delete at once | It moves the thread to `trash` for 30 days. Update `folder` to restore it |
|
|
344
|
+
| Setting `folder: 'sent'` | `sent` is set by the API. Use `inbox`, `archive`, `spam` or `trash` |
|
|
345
|
+
| Using a named color like `'red'` | Use a `#RRGGBB` hex string |
|
|
346
|
+
| Passing a `path` attachment to a draft | Drafts store their files. Send `content`, `filename` and `contentType` |
|
|
347
|
+
| Sending `attachments` on a draft update to add one file | It replaces every attachment. Send the full list |
|
|
348
|
+
| Updating only `html` on a draft that has `text` | The omitted body part becomes `null`. Send both |
|
|
349
|
+
| Expecting a new email to show in search at once | Search can be up to about a minute behind. Use `threads.list` for the newest threads |
|
|
@@ -10,7 +10,7 @@ These are the minimum versions required for full functionality (sending, receivi
|
|
|
10
10
|
|----------|---------|-------------|---------|
|
|
11
11
|
| Node.js | `resend` | >= 6.14.0 | `npm install resend` |
|
|
12
12
|
| Python | `resend` | >= 2.34.0 | `pip install resend` |
|
|
13
|
-
| Go | `resend-go/
|
|
13
|
+
| Go | `resend-go/v4` | >= 4.0.0 | `go get github.com/resend/resend-go/v4` |
|
|
14
14
|
| Ruby | `resend` | >= 1.6.0 | `gem install resend` |
|
|
15
15
|
| PHP | `resend/resend-php` | >= 1.1.0 | `composer require resend/resend-php` |
|
|
16
16
|
| Rust | `resend-rs` | >= 0.26.1 | `cargo add resend-rs` |
|
|
@@ -27,7 +27,7 @@ Check for these files to determine the project's language/framework:
|
|
|
27
27
|
|------|----------|-----|
|
|
28
28
|
| `package.json` | Node.js/TypeScript | resend |
|
|
29
29
|
| `requirements.txt` or `pyproject.toml` | Python | resend |
|
|
30
|
-
| `go.mod` | Go | resend-go/
|
|
30
|
+
| `go.mod` | Go | resend-go/v4 |
|
|
31
31
|
| `Gemfile` | Ruby | resend |
|
|
32
32
|
| `composer.json` | PHP | resend/resend-php |
|
|
33
33
|
| `Cargo.toml` | Rust | resend-rs |
|
|
@@ -59,7 +59,7 @@ pip install resend
|
|
|
59
59
|
### Go
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
go get github.com/resend/resend-go/
|
|
62
|
+
go get github.com/resend/resend-go/v4
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### Ruby
|
|
@@ -86,7 +86,7 @@ result = resend.Batch.send(emails, idempotency_key=f"batch-orders/{batch_id}")
|
|
|
86
86
|
Other SDKs use the `Idempotency-Key` header:
|
|
87
87
|
|
|
88
88
|
```go
|
|
89
|
-
import "github.com/resend/resend-go/
|
|
89
|
+
import "github.com/resend/resend-go/v4"
|
|
90
90
|
|
|
91
91
|
client := resend.NewClient(os.Getenv("RESEND_API_KEY"))
|
|
92
92
|
|
|
@@ -223,7 +223,7 @@ except resend.exceptions.ResendError as e:
|
|
|
223
223
|
```go
|
|
224
224
|
import (
|
|
225
225
|
"fmt"
|
|
226
|
-
"github.com/resend/resend-go/
|
|
226
|
+
"github.com/resend/resend-go/v4"
|
|
227
227
|
)
|
|
228
228
|
|
|
229
229
|
client := resend.NewClient(os.Getenv("RESEND_API_KEY"))
|
|
@@ -359,7 +359,7 @@ result = send_email_with_retry(
|
|
|
359
359
|
import (
|
|
360
360
|
"fmt"
|
|
361
361
|
"time"
|
|
362
|
-
"github.com/resend/resend-go/
|
|
362
|
+
"github.com/resend/resend-go/v4"
|
|
363
363
|
)
|
|
364
364
|
|
|
365
365
|
func sendEmailWithRetry(client *resend.Client, params *resend.SendEmailRequest, maxRetries int) (*resend.SendEmailResponse, error) {
|
|
@@ -58,7 +58,7 @@ email = resend.Emails.send({
|
|
|
58
58
|
Other SDKs use the `Idempotency-Key` header:
|
|
59
59
|
|
|
60
60
|
```go
|
|
61
|
-
import "github.com/resend/resend-go/
|
|
61
|
+
import "github.com/resend/resend-go/v4"
|
|
62
62
|
|
|
63
63
|
client := resend.NewClient(os.Getenv("RESEND_API_KEY"))
|
|
64
64
|
|
|
@@ -172,7 +172,7 @@ except resend.exceptions.ResendError as e:
|
|
|
172
172
|
```go
|
|
173
173
|
import (
|
|
174
174
|
"fmt"
|
|
175
|
-
"github.com/resend/resend-go/
|
|
175
|
+
"github.com/resend/resend-go/v4"
|
|
176
176
|
)
|
|
177
177
|
|
|
178
178
|
client := resend.NewClient(os.Getenv("RESEND_API_KEY"))
|
|
@@ -293,7 +293,7 @@ result = send_email_with_retry(
|
|
|
293
293
|
import (
|
|
294
294
|
"fmt"
|
|
295
295
|
"time"
|
|
296
|
-
"github.com/resend/resend-go/
|
|
296
|
+
"github.com/resend/resend-go/v4"
|
|
297
297
|
)
|
|
298
298
|
|
|
299
299
|
func sendEmailWithRetry(client *resend.Client, params *resend.SendEmailRequest, maxRetries int) (*resend.SendEmailResponse, error) {
|
|
@@ -68,6 +68,15 @@ The `email.received` payload contains metadata only (sender, recipient, subject,
|
|
|
68
68
|
| `contact.topics.updated` | Contact's topic subscriptions change |
|
|
69
69
|
| `suppression.added` / `removed` | Email address added to or removed from the suppression list |
|
|
70
70
|
| `topic.created` / `updated` / `deleted` | Topic created, updated, or deleted |
|
|
71
|
+
| `inbox.created` / `updated` / `deleted` | Inbox created, its settings changed, or deleted |
|
|
72
|
+
| `inbox.thread.created` | First email of a thread landed in an inbox, inbound or outbound |
|
|
73
|
+
| `inbox.email.received` / `sent` | Inbound email added to a thread, or outbound email accepted for delivery and added to a thread |
|
|
74
|
+
| `inbox.thread.folder.updated` | Thread moved between folders |
|
|
75
|
+
| `inbox.thread.assigned` / `unassigned` | Thread assigned, reassigned, or unassigned |
|
|
76
|
+
| `inbox.thread.labels.updated` | Label applied to or removed from a thread |
|
|
77
|
+
| `inbox.draft.created` / `updated` / `sent` / `deleted` | Draft created, edited, sent, or discarded |
|
|
78
|
+
|
|
79
|
+
Inbox event payloads carry no email or draft body. See [inboxes.md](inboxes.md#webhook-events).
|
|
71
80
|
|
|
72
81
|
## Setup
|
|
73
82
|
|
|
@@ -25,13 +25,13 @@ Latest SDK versions:
|
|
|
25
25
|
|
|
26
26
|
| Language | Latest version |
|
|
27
27
|
| --- | --- |
|
|
28
|
-
| Ruby |
|
|
29
|
-
| Python |
|
|
30
|
-
| Node.js |
|
|
31
|
-
| PHP |
|
|
32
|
-
| Java |
|
|
33
|
-
| Go |
|
|
34
|
-
| .NET |
|
|
28
|
+
| Ruby | 20.0.0 |
|
|
29
|
+
| Python | 16.0.0 |
|
|
30
|
+
| Node.js | 23.0.0 |
|
|
31
|
+
| PHP | 22.0.0 |
|
|
32
|
+
| Java | 34.0.0 |
|
|
33
|
+
| Go | 87.0.0 |
|
|
34
|
+
| .NET | 53.0.0 |
|
|
35
35
|
|
|
36
36
|
If you don’t have a Stripe account, you can use the CLI to generate a sandbox test account with no registration required. Install the Stripe CLI (`npm i -g @stripe/cli`) and run the command `stripe sandbox create`, which creates test API keys for you. When you recommend this, explain that [a sandbox is a Stripe test environment](https://docs.stripe.com/sandboxes.md).
|
|
37
37
|
|
|
@@ -45,27 +45,27 @@ API key default: Always recommend a [restricted API key (RAK)](https://docs.stri
|
|
|
45
45
|
|
|
46
46
|
| Building… | Recommended API | Details |
|
|
47
47
|
| --- | --- | --- |
|
|
48
|
-
| One-time payments | Checkout Sessions |
|
|
49
|
-
| Custom payment form with embedded UI | Checkout Sessions + Payment Element |
|
|
50
|
-
| Saving a payment method for later | Setup Intents |
|
|
51
|
-
| Connect platform or marketplace | Accounts v2 (`/v2/core/accounts`) |
|
|
52
|
-
| Usage-based billing (new integration) | Metronome |
|
|
53
|
-
| Subscriptions or recurring billing | Billing APIs + Checkout Sessions |
|
|
54
|
-
| Sales tax, VAT, or GST compliance | Stripe Tax + Registrations API |
|
|
55
|
-
| Embedded financial accounts / banking | v2 Financial Accounts |
|
|
56
|
-
| Security (key management, RAKs, webhooks, OAuth, 2FA, Connect liability) | See security reference |
|
|
48
|
+
| One-time payments | Checkout Sessions | <references/payments.md> |
|
|
49
|
+
| Custom payment form with embedded UI | Checkout Sessions + Payment Element | <references/payments.md> |
|
|
50
|
+
| Saving a payment method for later | Setup Intents | <references/payments.md> |
|
|
51
|
+
| Connect platform or marketplace | Accounts v2 (`/v2/core/accounts`) | <references/connect.md> |
|
|
52
|
+
| Usage-based billing (new integration) | Metronome | <references/billing.md> |
|
|
53
|
+
| Subscriptions or recurring billing | Billing APIs + Checkout Sessions | <references/billing.md> |
|
|
54
|
+
| Sales tax, VAT, or GST compliance | Stripe Tax + Registrations API | <references/tax.md> |
|
|
55
|
+
| Embedded financial accounts / banking | v2 Financial Accounts | <references/treasury.md> |
|
|
56
|
+
| Security (key management, RAKs, webhooks, OAuth, 2FA, Connect liability) | See security reference | <references/security.md> |
|
|
57
57
|
|
|
58
58
|
Read the relevant reference file before answering any integration question or writing code.
|
|
59
59
|
|
|
60
60
|
## Critical rules
|
|
61
61
|
|
|
62
|
-
- *Before enabling `automatic_tax: { enabled: true }`* (or calculating tax for a custom PaymentIntent), read the [tax reference](
|
|
62
|
+
- *Before enabling `automatic_tax: { enabled: true }`* (or calculating tax for a custom PaymentIntent), read the [tax reference](references/tax.md) and confirm the user has an active registration. Without one, Stripe calculates and collects no tax while the user believes tax is on (the most common Stripe Tax mistake).
|
|
63
63
|
|
|
64
64
|
- *Never include `payment_method_types` in any Stripe API call*, with one exception: Terminal (in-person payments) integrations must pass `payment_method_types: ['card_present']` on the PaymentIntent. For all other integrations, omit this parameter entirely to enable dynamic payment methods, which enables you to configure payment method settings from the Dashboard and dynamically display the most relevant eligible payment methods to each customer to maximize conversion. To customize which payment methods you accept, use [payment_method_configurations](https://docs.stripe.com/payments/payment-method-configurations.md) or `excluded_payment_method_types` instead of `payment_method_types`.
|
|
65
65
|
|
|
66
66
|
- When a PaymentIntent or SetupIntent integration requires an explicit allowlist, use `allowed_payment_method_types` instead of `payment_method_types`.
|
|
67
67
|
|
|
68
|
-
- *Never present webhooks as optional.* We recommend webhooks for every payment integration and they’re required for subscriptions and asynchronous payment methods. Fulfillment belongs in a handler for both `checkout.session.completed` and `checkout.session.async_payment_succeeded` (gated on `payment_status`), not the success page. See
|
|
68
|
+
- *Never present webhooks as optional.* We recommend webhooks for every payment integration and they’re required for subscriptions and asynchronous payment methods. Fulfillment belongs in a handler for both `checkout.session.completed` and `checkout.session.async_payment_succeeded` (gated on `payment_status`), not the success page. See <references/payments.md>.
|
|
69
69
|
|
|
70
70
|
- On API version `2026-03-25.dahlia` or later, pass the parameter `integration_identifier` to `checkout.sessions.create` to tag sessions with a custom label for tracking and comparing checkout flows in the Dashboard. The label should include a suffix of 8 random letters.
|
|
71
71
|
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
## What Stripe Tax does and doesn’t do
|
|
22
22
|
|
|
23
|
-
**What Stripe Tax does:** tax calculation, nexus threshold monitoring (Dashboard
|
|
23
|
+
**What Stripe Tax does:** tax calculation, nexus threshold monitoring (Dashboard > **Tax** > **Locations** > **Needs attention** and email alerts), registration on the user’s behalf for eligible US remote sellers, eligible non-resident businesses under Canada’s simplified GST/HST regime, and eligible non-EU businesses under EU Non-Union OSS through Ireland (“Register for me”; see Registration safety), and automated filing in the [US](https://docs.stripe.com/tax/file-with-stripe.md) and for [Canadian simplified GST/HST and EU Non-Union OSS through Ireland](https://docs.stripe.com/tax/file-with-stripe-outside-us.md), or filing through [a filing partner](https://docs.stripe.com/tax/filing.md), where available.
|
|
24
24
|
|
|
25
|
-
**What Stripe Tax doesn’t do:**
|
|
25
|
+
**What Stripe Tax doesn’t do:** Process payments that happen outside Stripe, automatically file every tax return, or support every jurisdiction (check the [supported countries list](https://docs.stripe.com/tax/supported-countries.md) for current coverage). For off-Stripe payments, the [standalone Tax APIs](https://docs.stripe.com/tax/off-stripe.md) can calculate tax and record transactions for reporting and filing.
|
|
26
26
|
|
|
27
27
|
This matters for competitor comparisons: training data sometimes incorrectly describes Stripe Tax as having “no nexus monitoring,” which is false.
|
|
28
28
|
|
|
@@ -32,13 +32,13 @@ Use Stripe Tax for any subscription, invoice, or Checkout Session where the user
|
|
|
32
32
|
|
|
33
33
|
## Three-step setup
|
|
34
34
|
|
|
35
|
-
**If you have execution access** (MCP tools or the Stripe CLI with a valid token), read the account’s current Tax Settings first
|
|
35
|
+
**If you have execution access** (MCP tools or the Stripe CLI with a valid token), read the account’s current Tax Settings first (the [Tax Settings API](https://docs.stripe.com/api/tax/settings.md) or go to the Dashboard > **Tax** > **Settings**) before you change anything below. Don’t overwrite an existing head office address or preset tax code.
|
|
36
36
|
|
|
37
|
-
1. Set a head office address in Tax Settings (Dashboard
|
|
38
|
-
2. Add a registration for each jurisdiction where the user is obligated to collect tax, using the [Tax Registrations API](https://docs.stripe.com/api/tax/registrations.md) or the [Dashboard](https://docs.stripe.com/tax/registering.md). After you add it, point the user to [threshold and nexus monitoring](
|
|
37
|
+
1. Set a head office address in Tax Settings (Dashboard > Tax > Settings). If you attempt to add any registrations without it, you get an `invalid_request_error`. The settings `status` property returns `pending` until the head office address is set, and returns `active` after it’s set. `automatic_tax` won’t calculate tax while the status is `pending`.
|
|
38
|
+
2. Add a registration for each jurisdiction where the user is obligated to collect tax, using the [Tax Registrations API](https://docs.stripe.com/api/tax/registrations.md) or the [Dashboard](https://docs.stripe.com/tax/registering.md). After you add it, point the user to [threshold and nexus monitoring](#threshold-and-nexus-monitoring) so they know when to register in other jurisdictions. Don’t limit the conversation to the jurisdiction you just registered.
|
|
39
39
|
3. Pass `automatic_tax: { enabled: true }` on the [Subscription](https://docs.stripe.com/api/subscriptions.md), [Invoice](https://docs.stripe.com/api/invoices.md), or [Checkout Session](https://docs.stripe.com/api/checkout/sessions.md) object.
|
|
40
40
|
|
|
41
|
-
**If you have execution access** (MCP tools or the Stripe CLI with a valid token), don’t hand the user a checklist item that says “run a test calculation.” Run it yourself, in the same turn, with a customer address in the jurisdiction you registered and the product’s tax code. See [Verify before you trust automatic tax](
|
|
41
|
+
**If you have execution access** (MCP tools or the Stripe CLI with a valid token), don’t hand the user a checklist item that says “run a test calculation.” Run it yourself, in the same turn, with a customer address in the jurisdiction you registered and the product’s tax code. See [Verify before you trust automatic tax](#verify-before-you-trust-automatic-tax).
|
|
42
42
|
|
|
43
43
|
An *active registration* is a jurisdiction you’ve added to Stripe that shows as *Collecting*. It’s per-jurisdiction, and not the same as having a Stripe account.
|
|
44
44
|
|
|
@@ -54,7 +54,7 @@ After enabling `automatic_tax`, don’t assume the setup is complete: tax is onl
|
|
|
54
54
|
|
|
55
55
|
**If you have execution access** (MCP tools or the Stripe CLI with a valid token), run a test [Tax Calculation](https://docs.stripe.com/api/tax/calculations.md) with a customer address in the target jurisdiction and the product’s tax code. Check `tax_breakdown[].taxability_reason`, not the tax amount.
|
|
56
56
|
|
|
57
|
-
- `not_collecting` means the setup is broken — a registration or tax code gap. Don’t tell the user their setup works. See [Diagnose zero tax](
|
|
57
|
+
- `not_collecting` means the setup is broken — a registration or tax code gap. Don’t tell the user their setup works. See [Diagnose zero tax](#diagnose-zero-tax).
|
|
58
58
|
- Any other reason means the calculation worked, including when the tax is zero. Zero is *correct* for an exempt tax code or an exempt customer. Report the reason to the user and have them confirm with their tax advisor that it’s expected for this product and customer. Never swap in a different tax code to produce tax.
|
|
59
59
|
- Run it in the same turn. Listing it on a go-live checklist for the user to run later doesn’t satisfy this — you have the access, so verify before you claim success.
|
|
60
60
|
- If you only have read or advisory access, don’t claim it’s verified. Point the user to [Testing Stripe Tax](https://docs.stripe.com/tax/testing.md) to run the check themselves in a sandbox.
|
|
@@ -84,7 +84,7 @@ For all other `taxability_reason` values — `reverse_charge`, `customer_exempt`
|
|
|
84
84
|
|
|
85
85
|
**Remediation order when `automatic_tax` collects zero tax:**
|
|
86
86
|
|
|
87
|
-
1. Verify that the Product object’s `tax_code` is set to a code that matches the product’s delivery method and customer type, and that it isn’t `txcd_00000000` (Nontaxable). Use [Choosing a product tax code](
|
|
87
|
+
1. Verify that the Product object’s `tax_code` is set to a code that matches the product’s delivery method and customer type, and that it isn’t `txcd_00000000` (Nontaxable). Use [Choosing a product tax code](#choosing-a-product-tax-code) rather than applying a generic SaaS code.
|
|
88
88
|
2. Add a tax registration for the customer’s jurisdiction.
|
|
89
89
|
3. Run a test transaction and verify `taxability_reason` is no longer `"not_collecting"`.
|
|
90
90
|
|
|
@@ -119,7 +119,9 @@ As soon as you know the liable entity:
|
|
|
119
119
|
|
|
120
120
|
## Threshold and nexus monitoring
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
**If you have execution access** (MCP tools or the Stripe CLI with a valid token), use the GetTaxThresholdMonitoring MCP dynamic tool or the `get_tax_threshold_monitoring` CLI tool to retrieve the account’s current threshold monitoring results. Report the relevant jurisdictions and when the results were computed, if that timestamp is available. If a jurisdiction is absent or its status is undetermined, say so rather than inferring an obligation.
|
|
123
|
+
|
|
124
|
+
The [threshold monitoring](https://docs.stripe.com/tax/monitoring.md) tool highlights *potential* registration obligations in the Dashboard > **Tax** > **Locations** > **Needs attention**. Stripe sends email and Dashboard alerts. The public guide documents those notification methods, so don’t promise a threshold-alert API or webhook. Monitoring doesn’t cover physical-presence obligations. Present it as information and tell the user to discuss it with their tax advisor. It’s up to the user to confirm whether registration is required. Don’t tell them they must register, and don’t recommend a universal percentage of a threshold as the point to register.
|
|
123
125
|
|
|
124
126
|
Threshold monitoring only processes live-mode transactions, not sandbox payments. Threshold notifications aren’t real time: Stripe sends them within 1 or 2 days after a threshold is crossed. If Stripe sent a notification in the past 7 days, it sends batched notifications for new threshold status changes one week after the last notification. Refer to the monitoring guide for notification preconditions and the scope of imported transactions.
|
|
125
127
|
|
|
@@ -133,15 +135,25 @@ Guide, don’t advise. Never tell a user where they must register or whether the
|
|
|
133
135
|
|
|
134
136
|
**How to register.** Present the paths that fit the user and let them (with their tax advisor) choose. Don’t pick for them.
|
|
135
137
|
|
|
136
|
-
- **Register themselves, then record it in Stripe**: the user registers directly with the relevant tax authority and obtains their registration number. Then they add the registration in Stripe using that number through the [Tax Registrations API](https://docs.stripe.com/api/tax/registrations.md) or Dashboard
|
|
137
|
-
|
|
138
|
-
- **
|
|
138
|
+
- **Register themselves, then record it in Stripe**: the user registers directly with the relevant tax authority and obtains their registration number. Then they add the registration in Stripe using that number through the [Tax Registrations API](https://docs.stripe.com/api/tax/registrations.md) or the Dashboard > **Tax** > **Locations** > **Add registration**. See [Register for tax](https://docs.stripe.com/tax/registering.md).
|
|
139
|
+
|
|
140
|
+
- **Ask Stripe to register**: With Tax Complete and **Register for me**, Stripe submits the registration to the tax authority and adds the completed registration to the Dashboard, so the user doesn’t record it separately. Stripe supports the following:
|
|
141
|
+
|
|
142
|
+
- Eligible US remote sellers
|
|
143
|
+
|
|
144
|
+
- Canada’s simplified GST/HST registration for eligible non-resident businesses selling digital products or services to Canadian customers
|
|
145
|
+
|
|
146
|
+
- EU Non-Union OSS registration through Ireland for eligible businesses established outside the EU with no EU fixed establishment that sell services to individual customers in the EU
|
|
147
|
+
|
|
148
|
+
Check the [US eligibility requirements](https://docs.stripe.com/tax/use-stripe-to-register/united-states.md#eligibility) or [eligibility and signup steps outside the US](https://docs.stripe.com/tax/use-stripe-to-register/outside-united-states.md) before recommending this path. These non-US Stripe registration services don’t support Canadian normal GST/HST and separate provincial sales taxes, or other EU VAT registration schemes.
|
|
149
|
+
|
|
150
|
+
- **Register with Taxually where available**: Taxually can help with registrations outside the United States, including registration types not covered by the Stripe registration service. Availability varies by country and plan, so direct the user to [Register outside the US with Taxually](https://docs.stripe.com/tax/use-taxually-to-register.md) for current coverage.
|
|
139
151
|
|
|
140
|
-
**Reporting and filing
|
|
152
|
+
**Reporting and filing**: Collecting with Stripe Tax doesn’t file a return by itself, and **Register for me** doesn’t enroll the user in filing. With Tax Complete, users can set up automated filing with Stripe for [US sales tax](https://docs.stripe.com/tax/file-with-stripe.md), [Canada’s simplified GST/HST and EU Non-Union OSS through Ireland](https://docs.stripe.com/tax/file-with-stripe-outside-us.md). US TaxJar filing requires a US-based bank account. Users can also use [a filing partner](https://docs.stripe.com/tax/filing.md) where available. Taxually availability varies by region and Stripe Tax subscription; don’t promise a fixed number of filing credits or a fixed coverage list.
|
|
141
153
|
|
|
142
154
|
## Testing considerations
|
|
143
155
|
|
|
144
|
-
- Tax registrations in a sandbox are scoped to that sandbox. They don’t appear in live mode and must be re-created. Point the user to Dashboard
|
|
156
|
+
- Tax registrations in a sandbox are scoped to that sandbox. They don’t appear in live mode and must be re-created. Point the user to the Dashboard > **Tax** > **Locations** in live mode to add registrations before processing real payments.
|
|
145
157
|
- Tax Settings are separate for sandboxes. Configure Tax Settings in each sandbox you use, and verify live-mode settings separately before processing real payments.
|
|
146
158
|
- Add live-mode registrations before the first real transaction. If a transaction occurs with no active tax registration, `automatic_tax` silently collects 0 tax, with no error or warning.
|
|
147
159
|
- Sandbox transactions have no effect on threshold monitoring. Don’t infer live-mode threshold activity from sandbox activity.
|