Shares & delivery
Share a succeeded render: create/list/revoke durable revokable share links (channel link|email, recipientEmail/message/expiresAt, shareUrl/shareToken), and email the document via Resend returning emailMessageId/sentAt. Covers 409 not_ready and 503 email_not_configured.
Share a succeeded render with recipients — mint durable revokable share links, list and revoke them, or send the document straight to an inbox via Resend. Shares are scoped to the render's project; the render must have status succeeded before any share or email can be created.
POST /v1/renders/:id/shares
GET /v1/renders/:id/shares
DELETE /v1/renders/:id/shares/:shareId
POST /v1/renders/:id/email:id is a render id (UUIDv7) within the authenticated project. :shareId is a share id returned by create or list.
Create a share
POST /v1/renders/:id/sharesMints a guest-facing share link. The render must have succeeded (409 not_ready otherwise). For channel: "email" the API only records the share row — it does not send mail; use POST /v1/renders/:id/email to actually deliver.
Quick Start
curl
curl -X POST https://api.craftkit.dev/v1/renders/$RENDER_ID/shares \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "link",
"message": "Here is your signed agreement.",
"expiresAt": "2026-12-31T23:59:59Z"
}'Node.js
const res = await fetch(`https://api.craftkit.dev/v1/renders/${renderId}/shares`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
channel: 'link',
message: 'Here is your signed agreement.',
expiresAt: '2026-12-31T23:59:59Z',
}),
});
const { shareUrl, shareToken } = await res.json();Python
import os, requests
res = requests.post(
f"https://api.craftkit.dev/v1/renders/{render_id}/shares",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
json={
"channel": "link",
"message": "Here is your signed agreement.",
"expiresAt": "2026-12-31T23:59:59Z",
},
)
share = res.json()Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string | Render id (UUIDv7) within the authenticated project. Required. | — |
Request body
{
"channel": "link",
"recipientEmail": "client@acme.com",
"message": "Here is your signed agreement.",
"expiresAt": "2026-12-31T23:59:59Z"
}| Field | Type | Description | Default |
|---|---|---|---|
channel |
string | link or email. link is a plain copy-and-paste URL; email tags the share as email-destined but does not send — it requires recipientEmail and you must call POST /v1/renders/:id/email to deliver. |
link |
recipientEmail |
string | Recipient email (must be a valid address). Required when channel is email, otherwise optional metadata. |
— |
message |
string | Optional note shown to the recipient. Max 2000 chars. | — |
expiresAt |
string | ISO-8601 timestamp. After this instant the share resolves to not-found on the public side. Omit for no auto-expiry. | No expiry |
Response — 201 Created
{
"id": "0193c2c3-...",
"shareToken": "cks_8sd9...",
"shareUrl": "https://www.craftkit.dev/share/cks_8sd9...",
"channel": "link",
"recipientEmail": null,
"message": "Here is your signed agreement.",
"expiresAt": "2026-12-31T23:59:59.000Z",
"revokedAt": null,
"createdAt": "2026-06-05T10:05:00.000Z"
}| Field | Type | Description |
|---|---|---|
id |
string | Share id. Pass to DELETE to revoke. |
shareToken |
string | Opaque token (prefixed cks_) embedded in shareUrl. Stored hashed server-side. |
shareUrl |
string | Public link: {shareBase}/share/{shareToken}. The base resolves to the partner's custom domain, then SHARE_BASE_URL, then APP_URL, then the request origin. |
channel |
string | link or email. |
recipientEmail |
string | null | Echoed recipient, or null for a plain link. |
message |
string | null | Echoed message. |
expiresAt |
string | null | ISO-8601 expiry, or null. |
revokedAt |
string | null | Always null on create. |
createdAt |
string | ISO-8601 creation timestamp. |
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 400 | invalid_json |
Body wasn't valid JSON | Check Content-Type and JSON.stringify |
| 400 | invalid_request |
Body failed schema validation, or channel="email" without recipientEmail |
Inspect issues; supply recipientEmail for the email channel |
| 401 | unauthorized |
Missing/invalid/revoked key | Send a valid Authorization: Bearer key |
| 404 | not_found |
No render with that id in this key's project | Check the id and the key's project scope |
| 409 | not_ready |
Render has not succeeded yet | Poll the render until status is succeeded |
List shares
GET /v1/renders/:id/sharesReturns every share for the render, newest first, including revoked ones. Each row carries a click count (recipient viewed events attributed to the share).
Quick Start
curl
curl https://api.craftkit.dev/v1/renders/$RENDER_ID/shares \
-H "Authorization: Bearer $CRAFTKIT_API_KEY"Node.js
const res = await fetch(`https://api.craftkit.dev/v1/renders/${renderId}/shares`, {
headers: { Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}` },
});
const { shares } = await res.json();Python
import os, requests
res = requests.get(
f"https://api.craftkit.dev/v1/renders/{render_id}/shares",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
)
shares = res.json()["shares"]Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string | Render id (UUIDv7) within the authenticated project. Required. | — |
Response — 200 OK
{
"shares": [
{
"id": "0193c2c3-...",
"channel": "link",
"recipientEmail": null,
"message": "Here is your signed agreement.",
"revokedAt": null,
"revokedReason": null,
"expiresAt": "2026-12-31T23:59:59.000Z",
"createdAt": "2026-06-05T10:05:00.000Z",
"shareToken": "cks_8sd9...",
"clickCount": 3,
"shareUrl": "https://www.craftkit.dev/share/cks_8sd9..."
}
]
}| Field | Type | Description |
|---|---|---|
shares |
array | Shares for the render, newest first. Revoked shares are included. |
shares[].id |
string | Share id. |
shares[].channel |
string | link or email. |
shares[].recipientEmail |
string | null | Recipient, or null. |
shares[].message |
string | null | Message shown to the recipient. |
shares[].revokedAt |
string | null | ISO-8601 revocation timestamp, or null if active. |
shares[].revokedReason |
string | null | Reason recorded at revoke time (e.g. revoked_by_partner), or null. |
shares[].expiresAt |
string | null | ISO-8601 expiry, or null. |
shares[].createdAt |
string | ISO-8601 creation timestamp. |
shares[].shareToken |
string | Plain token, re-exposed so a dashboard can re-display copy-link rows. |
shares[].clickCount |
integer | Count of recipient viewed events attributed to this share. |
shares[].shareUrl |
string | {shareBase}/share/{shareToken}. |
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 401 | unauthorized |
Missing/invalid/revoked key | Send a valid Authorization: Bearer key |
| 404 | not_found |
No render with that id in this key's project | Check the id and the key's project scope |
Revoke a share
DELETE /v1/renders/:id/shares/:shareIdSoft-deletes the share by stamping revokedAt (reason revoked_by_partner). The public /share/:token URL stops resolving immediately. Revoking is idempotent only in the sense that an already-revoked or unknown share returns 404 — a share can be revoked once.
Quick Start
curl
curl -X DELETE https://api.craftkit.dev/v1/renders/$RENDER_ID/shares/$SHARE_ID \
-H "Authorization: Bearer $CRAFTKIT_API_KEY"Node.js
const res = await fetch(
`https://api.craftkit.dev/v1/renders/${renderId}/shares/${shareId}`,
{
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}` },
},
);
const { revokedAt } = await res.json();Python
import os, requests
res = requests.delete(
f"https://api.craftkit.dev/v1/renders/{render_id}/shares/{share_id}",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
)
result = res.json()Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string | Render id (UUIDv7) within the authenticated project. Required. | — |
shareId |
string | Share id to revoke. Required. | — |
Response — 200 OK
{
"id": "0193c2c3-...",
"revokedAt": "2026-06-06T09:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
id |
string | The revoked share id. |
revokedAt |
string | ISO-8601 revocation timestamp. |
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 401 | unauthorized |
Missing/invalid/revoked key | Send a valid Authorization: Bearer key |
| 404 | not_found |
Render not found, or share not found / already revoked | Confirm both ids; a share can only be revoked once |
Email a render
POST /v1/renders/:id/emailCreates an email-channel share and sends the document via Resend in one call. The render must have succeeded. Returns the Resend message id. This endpoint requires email to be configured: RESEND_API_KEY plus a sender address (EMAIL_FROM, or a per-partner emailFrom) — otherwise it returns 503 email_not_configured.
Quick Start
curl
curl -X POST https://api.craftkit.dev/v1/renders/$RENDER_ID/email \
-H "Authorization: Bearer $CRAFTKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "client@acme.com",
"recipientName": "Jane Doe",
"message": "Here is your signed agreement.",
"expiresAt": "2026-12-31T23:59:59Z"
}'Node.js
const res = await fetch(`https://api.craftkit.dev/v1/renders/${renderId}/email`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRAFTKIT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: 'client@acme.com',
recipientName: 'Jane Doe',
message: 'Here is your signed agreement.',
expiresAt: '2026-12-31T23:59:59Z',
}),
});
const { emailMessageId, sentAt } = await res.json();Python
import os, requests
res = requests.post(
f"https://api.craftkit.dev/v1/renders/{render_id}/email",
headers={"Authorization": f"Bearer {os.environ['CRAFTKIT_API_KEY']}"},
json={
"to": "client@acme.com",
"recipientName": "Jane Doe",
"message": "Here is your signed agreement.",
"expiresAt": "2026-12-31T23:59:59Z",
},
)
result = res.json()Path parameters
| Field | Type | Description | Default |
|---|---|---|---|
id |
string | Render id (UUIDv7) within the authenticated project. Required. | — |
Request body
{
"to": "client@acme.com",
"recipientName": "Jane Doe",
"message": "Here is your signed agreement.",
"expiresAt": "2026-12-31T23:59:59Z"
}| Field | Type | Description | Default |
|---|---|---|---|
to |
string | Recipient email (must be a valid address). Required. | — |
recipientName |
string | Recipient display name used in the email greeting. Max 200 chars. | — |
message |
string | Optional note included in the email body. Max 2000 chars. | — |
expiresAt |
string | ISO-8601 timestamp. Sets an expiry on the underlying share link. Omit for no auto-expiry. | No expiry |
Response — 201 Created
{
"id": "0193c2c3-...",
"shareToken": "cks_8sd9...",
"shareUrl": "https://www.craftkit.dev/share/cks_8sd9...",
"emailMessageId": "re_abc123",
"sentAt": "2026-06-06T09:05:00.000Z"
}| Field | Type | Description |
|---|---|---|
id |
string | The created share id (channel email). |
shareToken |
string | Opaque token embedded in shareUrl. |
shareUrl |
string | The link delivered in the email: {shareBase}/share/{shareToken}. |
emailMessageId |
string | Resend provider message id for the sent email. |
sentAt |
string | ISO-8601 timestamp the email was dispatched. |
Errors
| HTTP | Code | Meaning | Fix |
|---|---|---|---|
| 400 | invalid_json |
Body wasn't valid JSON | Check Content-Type and JSON.stringify |
| 400 | invalid_request |
Body failed schema validation (missing/invalid to, etc.) |
Inspect issues; provide a valid to address |
| 401 | unauthorized |
Missing/invalid/revoked key | Send a valid Authorization: Bearer key |
| 404 | not_found |
No render with that id in this key's project | Check the id and the key's project scope |
| 409 | not_ready |
Render has not succeeded yet | Poll the render until status is succeeded |
| 502 | email_send_failed |
Resend rejected the send | Inspect the error message; verify sender domain and recipient |
| 503 | email_not_configured |
Resend isn't wired on this deployment | Set RESEND_API_KEY + EMAIL_FROM (or a per-partner sender) |
Related
- POST /v1/templates/:slug/render — enqueue the render to share
- GET /v1/renders/:id — poll the render to
succeededbefore sharing - Errors — error envelope and retry semantics
- Authentication — bearer token format