Skip to content
ChartDeck ChartScript docs Account Open the terminal

Selling access

Sell access to an invite-only script, in ChartDeck with Stripe or on your own site with the vendor API, access codes and signed webhooks.

An invite-only script can be something people pay for. You give each buyer access for a term (a month, a year, for life, and so on), and ChartDeck lets them in and out at the right times. There are two ways to sell. You can use either, or neither, for each script.

A: Sell in ChartDeckB: Sell on your own site
Who takes the moneyYour own Stripe account, connected to ChartDeckYour shop, course platform or membership site
Who lets the buyer inChartDeck, when the payment succeedsYour backend, through the vendor API, or the buyer with an access code
Renewals and refundsFollow the Stripe subscriptionYour backend extends or removes access
On the script's cardBuy with your pricesGet access, linking to a page on your verified site

Both options only work on invite-only scripts. A public script can be added by anyone, so there is nothing to sell.

#Terms

Every way of giving access (your Access panel, an invite link, the API, an access code, a purchase) uses the same terms:

TermAccess ends
1mOne calendar month later (UTC). 31 January plus one month is 28 or 29 February.
3mThree calendar months later.
1yOne year later.
lifetimeNever.
customOn an exact date you give (expires_at), or after a number of days. Up to 3650 days ahead.
trialAfter days (1 to 30, default 7). Each member gets one trial per script, ever.

What happens when you give access again:

  • Grant starts the term now, but never shortens access that is still running. A member with lifetime access keeps it; a member whose access runs longer than the new term keeps the later end.
  • Extend adds the term on top of the current end (or starts now if their access has ended). Lifetime stays lifetime.
  • Set puts the end exactly where you say, even if that is sooner. Setting it to nothing means lifetime.

When a term ends, the script stays on the member's charts but stops running and says "Access expired". Giving them access again brings it straight back. Removing someone's access (revoking it) says "Access removed by the author".

#Option A: sell in ChartDeck

  1. Open Account → Selling & access, choose your country and connect your Stripe account. Stripe asks for the details it needs to pay you. ChartDeck never sees your bank details.
  2. On the script, add one or more prices: monthly, yearly and/or lifetime (one payment), in your currency.
  3. Set the script's selling mode to Sell in ChartDeck. You can offer new buyers a free trial of 1 to 30 days.
  4. In your own Stripe Dashboard, save a Customer portal setup (Settings → Billing → Customer portal), so buyers can manage or cancel their subscription.

Buyers see Get access with your prices on the script's card. Payment runs on your Stripe account (you are the seller): the money, receipts, taxes, refunds and disputes are yours, in your Stripe Dashboard. ChartDeck takes no fee: you keep the whole sale (only Stripe's own processing fees apply, on your Stripe account).

  • A subscription keeps access running while it renews: access lasts to the end of the paid period plus 3 days for payment retries.
  • A canceled subscription ends access at the end of the paid period ("Access expired").
  • A full refund or an opened dispute removes access at once. A won dispute gives it back. A partial refund changes nothing.
  • Buyers see their purchases, renewal dates and a Manage button under Account → My subscriptions.

#Option B: sell on your own site

You keep selling where you already sell, and tell ChartDeck who paid. There are two ways to let a buyer in:

  • By @handle or ChartDeck ID. If your checkout asks for the buyer's ChartDeck @handle (or their ChartDeck ID, shown under Account → My subscriptions), your backend grants access with the API straight away.
  • With an access code. If you only have the buyer's email, create a single-use code and email it to them yourself. They redeem it in ChartDeck. You never need to know who they are in ChartDeck.

#Privacy: no emails

ChartDeck never tells anyone whether an email address has an account. The API refuses emails (422 email_not_supported) and never returns one. Members are always { "id": "<ChartDeck ID>", "handle": "<handle or null>" }. The handle is only shown when the member's profile is public. Your private note on a member is never shown to them.

#API keys

Create a key under Account → Selling & access → API keys. The key looks like cdv_ followed by 43 characters and is shown once: copy it into your server's secret store. ChartDeck keeps only a fingerprint of it, so a lost key cannot be shown again. Make a new one instead.

  • Up to 10 active keys. A key can be limited to some of your scripts; otherwise it manages all your invite-only scripts.
  • Rotate makes a new key with the same name and scope. The old key stops working at once, so put the new key in place straight after rotating.
  • Revoke stops a key for good.

Send the key in the Authorization header of every call. Keep it on your server: never put it in a web page or an app. The API ignores ChartDeck sign-in cookies, so being signed in to ChartDeck in the same browser grants nothing.

Shell
export CD_KEY="cdv_your_key_here"
curl https://chartdeck.trade/api/v1/vendor/me -H "Authorization: Bearer $CD_KEY"
JSON
{ "author": { "id": "6f1c...", "handle": "yourhandle" }, "key": { "id": "...", "name": "My shop", "prefix": "cdv_Ab3dE9fG", "scripts": null } }

The full description of the API, for code generators and API tools, is the OpenAPI document at https://chartdeck.trade/api/v1/vendor/openapi.json (no key needed).

#Listings

A listing id is your script's public id (the 26 characters in its Community link).

Shell
curl https://chartdeck.trade/api/v1/vendor/listings -H "Authorization: Bearer $CD_KEY"
JSON
{ "listings": [ { "id": "01j9x0abcdefghjkmnpqrstvwx", "name": "Ribbon", "published": true, "listed": false, "members": 42 } ] }

Your key only reaches your own invite-only scripts. Another author's script is 404 not_found (the API never says whether it exists), one of yours outside the key's scope is 403 forbidden_listing, and one of yours with a public audience is 409 not_invite_only.

#Grant access

Shell
curl -X POST https://chartdeck.trade/api/v1/vendor/listings/01j9x0abcdefghjkmnpqrstvwx/members \
  -H "Authorization: Bearer $CD_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001" \
  -d '{ "user": "@buyerhandle", "term": "1m", "note": "order 1001" }'

user is "@handle" or a ChartDeck ID. The answer is 201 when the member was let in, 200 when existing access was extended or did not change:

JSON
{
  "result": "granted",
  "member": {
    "user": { "id": "3b2a...", "handle": "buyerhandle" }, "listing": "01j9x0abcdefghjkmnpqrstvwx",
    "source": "api", "term": "1m", "trial": false, "status": "active",
    "expiresAt": "2026-11-09T12:00:00.000Z", "since": "2026-10-09T12:00:00.000Z",
    "updatedAt": "2026-10-09T12:00:00.000Z", "revokedAt": null, "note": "order 1001"
  }
}

A custom end and a trial:

Shell
-d '{ "user": "@buyerhandle", "term": "custom", "expires_at": "2027-03-31T23:59:59Z" }'
-d '{ "user": "@buyerhandle", "term": "trial", "days": 7 }'

A script can have up to 1000 members with live access. Granting to a new member past that is 409 members_limit.

#Renew, change and remove

Shell
# a renewal: add a month on top of the current end
curl -X POST https://chartdeck.trade/api/v1/vendor/listings/01j9x0abcdefghjkmnpqrstvwx/members/%40buyerhandle/extend \
  -H "Authorization: Bearer $CD_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: invoice-2002" \
  -d '{ "term": "1m" }'

# set an exact end (null = lifetime) and/or your note
curl -X PATCH https://chartdeck.trade/api/v1/vendor/listings/01j9x0abcdefghjkmnpqrstvwx/members/%40buyerhandle \
  -H "Authorization: Bearer $CD_KEY" -H "Content-Type: application/json" \
  -d '{ "expires_at": "2026-12-31T23:59:59Z", "note": "upgraded" }'

# remove access (a refund, a cancellation); calling it again changes nothing
curl -X DELETE https://chartdeck.trade/api/v1/vendor/listings/01j9x0abcdefghjkmnpqrstvwx/members/%40buyerhandle \
  -H "Authorization: Bearer $CD_KEY"

In a URL path, write the @ as %40, or use the member's ChartDeck ID.

#Look up members

Shell
curl "https://chartdeck.trade/api/v1/vendor/listings/01j9x0abcdefghjkmnpqrstvwx/members?status=active&limit=100" \
  -H "Authorization: Bearer $CD_KEY"
curl https://chartdeck.trade/api/v1/vendor/listings/01j9x0abcdefghjkmnpqrstvwx/members/%40buyerhandle \
  -H "Authorization: Bearer $CD_KEY"

status is active, expired, revoked or all (the default). limit is 1 to 200 (default 50). When there are more, the answer has a next value: pass it back as cursor for the next page. Asking about someone who was never a member is 404 member_not_found.

#Access codes

A code is single use and looks like CDA-7K3M-Q9TX-2HV4-PR8N. The access term starts when the buyer redeems it, not when you create it. The code itself must be redeemed within code_valid_days (1 to 365, default 30).

Shell
curl -X POST https://chartdeck.trade/api/v1/vendor/listings/01j9x0abcdefghjkmnpqrstvwx/codes \
  -H "Authorization: Bearer $CD_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: order-1003" \
  -d '{ "term": "1y", "external_ref": "order-1003", "quantity": 1 }'
JSON
{ "codes": [ { "id": "9d0e...", "code": "CDA-7K3M-Q9TX-2HV4-PR8N", "url": "https://chartdeck.trade/redeem?code=CDA-7K3M-Q9TX-2HV4-PR8N",
  "term": "1y", "days": null, "until": null, "expiresAt": "2026-11-08T12:00:00.000Z", "externalRef": "order-1003" } ] }
  • The code is returned only here: ChartDeck keeps just a fingerprint and the last four characters. Send the code or its url to your buyer.
  • The buyer opens the link (signing in or signing up first) or types the code in ChartDeck. Capitals, spaces and dashes do not matter, and O / I / L are read as 0 / 1 / 1.
  • external_ref is your own order id. It comes back in the webhook when the code is redeemed, so you can match the two.
  • quantity makes up to 100 codes at once.
  • A code does not let back in a member you removed (with DELETE .../members/<user> or Remove / Block in the Access panel): they get 403 access_removed and the code stays unused. To let them in again, grant them access yourself (POST .../members, or the Access panel). A member who lost access through a refund or a dispute can come back with a new code.
  • List codes with GET .../codes?status=unused|redeemed|revoked|expired|all (never with the code itself; a redeemed one shows who redeemed it). Turn off an unused code with DELETE /api/v1/vendor/codes/<id>. A redeemed code cannot be turned off (409 already_redeemed); remove the member's access instead.

You can also make codes yourself, without the API, in the script's Access panel.

#The access log

Every change to your members (from the API, codes, your Access panel, purchases and expiries) is in the log, oldest first:

Shell
curl "https://chartdeck.trade/api/v1/vendor/events?after=0&limit=100" -H "Authorization: Bearer $CD_KEY"
JSON
{ "events": [ { "id": 812, "type": "access.granted", "listing": "01j9x0abcdefghjkmnpqrstvwx", "user": { "id": "3b2a...", "handle": "buyerhandle" },
  "source": "api", "actor": "api", "term": "1m", "expiresAt": "2026-11-09T12:00:00.000Z", "prevExpiresAt": null, "ref": null,
  "at": "2026-10-09T12:00:00.000Z" } ], "next": 812 }

Pass next as after to read on. The types are access.granted, access.extended, access.revoked, access.expired and access.requested (a member asked for access to a listed script).

#Idempotency

Every POST, PATCH and DELETE accepts an Idempotency-Key header: any unique text up to 255 characters, such as your order or invoice id. If your request times out, send it again with the same key. ChartDeck keeps the first answer for 24 hours and sends it back with the header Idempotent-Replayed: true, so a retry never grants twice. The same key with a different request is 422 idempotency_mismatch; a retry while the first is still running is 409 idempotency_in_progress. Keys are per API key.

#Rate limits

Each API key can make 120 reads and 60 changes a minute. Over that, the answer is 429 rate_limited with a Retry-After header (seconds). Wait that long and retry.

#Errors

Errors are JSON with a status code:

JSON
{ "error": { "code": "email_not_supported", "message": "Use the member’s @handle or ChartDeck ID, or create an access code and send it to your buyer." } }
StatusCodes
401unauthorized: no key, a wrong key or a revoked key
403forbidden_listing: the key is limited to other scripts
404not_found, user_not_found, member_not_found, code_not_found, no_endpoint
409self (you always have access to your own script), members_limit, trial_used, not_invite_only, already_redeemed, endpoint_disabled, idempotency_in_progress
422invalid, invalid_term, email_not_supported, idempotency_mismatch
429rate_limited

#Webhooks

ChartDeck can tell your server whenever access changes. Under Account → Selling & access → Webhook enter an https:// address on your server and choose the events. The signing secret (whsec_cd_ and 43 characters) is shown once; Roll secret makes a new one.

Each event is a POST with a JSON body:

JSON
{
  "id": "evt_0h5x2k9q7m3t1v8c4b6n0r2w5y",
  "type": "access.granted",
  "created": 1791633600,
  "data": {
    "listing": "01j9x0abcdefghjkmnpqrstvwx", "user": { "id": "3b2a...", "handle": "buyerhandle" },
    "source": "api", "actor": "member", "term": "1y", "expires_at": "2027-10-09T12:00:00.000Z", "previous_expires_at": null,
    "ref": "9d0e...", "code": "9d0e...", "external_ref": "order-1003"
  }
}
  • Answer with any 2xx status, quickly. Anything else (a redirect too) counts as a failure and is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours; after 8 tries it is given up.
  • The id is the same on every retry, and when you press Resend on a delivery. Use it to ignore duplicates.
  • If every delivery fails for 3 days, ChartDeck turns the webhook off and tells you in your notifications. Fix your server and save the webhook again: waiting events then go out.
  • POST /api/v1/vendor/webhook/test sends an access.test event so you can check your endpoint.
  • Every request carries ChartDeck-Event-Id, ChartDeck-Event-Type and ChartDeck-Signature.

Check the signature before trusting a webhook. ChartDeck-Signature is t=<unix time>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw body> with your secret. Use the raw body exactly as it arrived, before any JSON parsing. A Node.js example:

js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyChartDeckSignature(rawBody, header, secret, toleranceSec = 300) {
  if (!header || !secret) return false;
  let t = NaN;
  const sigs = [];
  for (const part of header.split(',')) {
    const [k, v] = part.split('=');
    if (k === 't') t = Number(v);
    else if (k === 'v1') sigs.push(v);
  }
  if (!Number.isFinite(t) || !sigs.length) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;   // too old: a replay
  const want = Buffer.from(createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'));
  return sigs.some((s) => { const got = Buffer.from(s); return got.length === want.length && timingSafeEqual(got, want); });
}

// Express: keep the raw body for this route
app.post('/chartdeck/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyChartDeckSignature(req.body.toString('utf8'), req.get('ChartDeck-Signature'), process.env.CHARTDECK_WEBHOOK_SECRET)) {
    return res.status(400).end();
  }
  const event = JSON.parse(req.body.toString('utf8'));
  // ignore event.id if you have seen it before, then act on event.type
  res.status(200).end();
});

With option B, the script's card can show one Get access button that opens a page on your site. To keep members safe it may only point to a site you have proved is yours.

  1. Under Account → Selling & access → Your site enter your domain, for example myshop.com. ChartDeck shows a token.
  2. Prove you control the domain in one of two ways:
    • DNS: add a TXT record named _chartdeck-verify.myshop.com with the value chartdeck-verify=<token>.
    • File: put a text file at https://myshop.com/.well-known/chartdeck-verify.txt that contains just the token. It must be served over https; redirects are followed only within your domain.
  3. Press Verify. DNS changes can take a while to appear; you can try 10 times an hour.
  4. In the script's selling options choose Sell on my site and paste the page address. It must start with https:// and be on your domain or one of its subdomains (shop.myshop.com is fine).

Changing the domain needs a new verification, and the button is hidden until it is done. The selling options also have an optional free-trial length you can mention on your page.

The button only takes buyers to your page. Letting them in is still done by your backend (a grant or a code).

The ChartScript documentation is in English for now; translations will follow.