Back to home

Connect your own product

The codedock.source/1 contract: three read-only GET endpoints, one token, full pulls — how a product that issues its own codes hands them to CodeDock.

Who should connect

The contract answers one question: how does a product that issues its own promo codes hand those codes to CodeDock.

“Issues its own” is the only requirement — where the product runs does not matter: a SaaS website, an Android app with a redeem entry, desktop software sold as license codes, a game handing out gift codes, an iOS app selling subscriptions on the web. As long as your own backend issues and redeems the codes, it applies. App Store promo codes and offer codes do not go through this contract; they are imported as files.

Products you can connect

WebsitesMobile appsDesktop softwareGames

The contract is deliberately tiny — closer to RSS than to OAuth: no callbacks, no authorization page, no middle server. The implementer is usually a CodeDock user themselves.

The connection

In CodeDock, the user fills in two things: a base URL and a token. Every request then looks like this:

GET {base}/v1/manifest HTTP/1.1
Authorization: Bearer cdk_ro_9f2c…
Accept: application/json
FieldExampleNotes
Base URLhttps://inkwell.app/api/codedockThe {base} below, without /v1
Tokencdk_ro_9f2c…Sent as-is in the Authorization header
  • {base} must be https. http://localhost, 127.0.0.1, and *.local are excepted for local development — a token crossing the public internet in cleartext is as good as pasted into a chat.
  • The token must be read-only and scoped to the three endpoints in this contract.
  • CodeDock stores it in the device Keychain and keeps it out of backup files — after restoring onto a new phone it has to be entered again, on purpose.

The three endpoints

All three endpoints accept GET only and return application/json (UTF-8). All timestamps are ISO 8601 with a timezone (UTC recommended).

The client ignores fields it does not recognize, so within v1 things only get added: new fields never require a version bump.

GET /v1/manifest

Who you are, and what you can report.

{
  "spec": "codedock.source/1",
  "product": {
    "id": "prod_inkwell",
    "name": "Inkwell",
    "website_url": "https://inkwell.app",
    "icon_url": "https://inkwell.app/icon.png",
    "redeem_url_template": "https://inkwell.app/redeem?code={code}"
  },
  "capabilities": ["redemption_counts"]
}
FieldRequiredNotes
specYesFixed at codedock.source/1. A mismatch and CodeDock refuses the connection
product.idYesA stable product ID. If the same connection switches tokens and starts pointing at another product, CodeDock refuses the sync
product.nameYesThe default name when the profile is created. The user can change it afterwards
product.website_urlNoProduct homepage
product.icon_urlNoA square PNG/JPEG, downloaded once at profile creation and resized to 128×128
product.redeem_url_templateNoRedemption link template, must contain {code}. CodeDock percent-encodes the code, substitutes it, and prints it as the QR code on share cards. https is recommended (App Links / Universal Links open your app directly); a custom scheme like inkwell://redeem?code={code} also works and only opens for people who have the app installed. javascript:, data:, and file: are rejected. Without it, cards carry no QR code
capabilitiesYesCapability list, may be an empty array. v1 defines exactly one: redemption_counts — every code carries times_redeemed. With it declared, “redeemed” is whatever your backend says and the owner can no longer mark it manually; without it, CodeDock shows no redemption counts (unknown is not zero)

GET /v1/offers

Offers. One offer maps to one batch of codes in CodeDock. Paginated, see below.

{
  "data": [
    {
      "id": "offer_press",
      "name": "Press preview",
      "summary": "3 months of Pro, free",
      "usage": "single_use",
      "status": "active",
      "expires_at": "2026-12-31T23:59:59Z"
    }
  ],
  "next_cursor": null
}
FieldRequiredNotes
idYesStable ID, unique within your source and never reused
nameYesBecomes the batch name on first sync only; afterwards the owner manages it
summaryNoThe one line printed for the recipient (e.g. “3 months of Pro free”). It appears on share cards — not the place for internal codenames
usageYessingle_use: one code, one person, void after one redemption (like an Apple promo code); multi_use: one code, many people (like LAUNCH50). It decides how CodeDock keeps the books and must not change after the offer is created
statusYesactive or archived. Codes of an archived offer are all treated as disabled
expires_atNoOffer deadline; null means no deadline

GET /v1/codes

Codes. Paginated, see below.

{
  "data": [
    {
      "id": "code_8x2k",
      "offer_id": "offer_press",
      "code": "INK-7KQ2-M9XD",
      "status": "active",
      "max_redemptions": 1,
      "times_redeemed": 0,
      "last_redeemed_at": null
    }
  ],
  "next_cursor": "b2Zmc2V0OjEwMA"
}
FieldRequiredNotes
idYesStable ID, unique within your source and never reused
offer_idYesThe owning offer. Codes pointing at a nonexistent offer are ignored
codeYesThe code itself. For the same id, code must never change
statusYesactive or disabled
max_redemptionsYesRedemption cap; null means unlimited. Under a single_use offer it must be 1
times_redeemedRequired when redemption_counts is declaredCumulative redemptions
last_redeemed_atNoMost recent redemption time; CodeDock shows it as “redeemed at”

Pagination

/v1/offers and /v1/codes both paginate by cursor:

GET {base}/v1/codes?limit=100
GET {base}/v1/codes?limit=100&cursor=b2Zmc2V0OjEwMA
  • limit defaults to 100 and caps at 500.
  • A null next_cursor in the response means the end. Cursors are opaque to the client.
  • Within a single sync, the server should keep pages stable — no duplicates, no gaps (sorting by id or creation time is enough).

Sync semantics — how CodeDock reads your data

Every sync pulls all pages of both collections and reconciles the local side against that snapshot. v1 has no incremental sync (no updated_since) and therefore needs no tombstones: absent from the snapshot means deleted.

CodeDock never writes back: hand out a code in the app and your server never knows. Single-use codes are best handed out through CodeDock alone; if the same code goes out twice from two places, CodeDock cannot deduplicate that for you.

If any page returns non-2xx, times out, or fails to parse, the whole sync is abandoned and local data stays untouched. The UI says “last sync failed · data is 3 hours old” instead of pretending the data is fresh.

Frequency: CodeDock syncs when the app returns to the foreground and the last attempt is more than 10 minutes old; pull-to-refresh syncs immediately. It does not poll in the background.

Who decides what

Your server decidesCodeDock (the owner) decides
Offer deadline, the line shown to recipients, whether it is archivedBatch name, tags, expiry reminders, owner
Whether a code is usable, its redemption cap, its redemption countWho it went to, through which channel, under which offer
Redemption link template, product homepageProfile name, icon

How states change

  • A single-use code with times_redeemed ≥ 1 that CodeDock handed out: marked “redeemed” and leaves the follow-up list.
  • A single-use code with times_redeemed ≥ 1 that CodeDock never handed out: recorded as “redeemed elsewhere” — out of stock, counted as redeemed, attributed to no one.
  • times_redeemed falling back to 0 (e.g. a refund reverting a redemption): the “redeemed” mark is undone and the code returns to sent-but-unredeemed.
  • A code disabled, an offer archived, or a code missing from the snapshot: un-sent ones leave the stock; sent ones stay on the record, marked disabled or removed.
  • Deleting a person never deletes the record; the server deleting a code does not either. “Who you sent it to in March” has already happened.

Errors

Express the category with an HTTP status code and give one readable reason in the body, e.g. {"error":{"code":"invalid_token","message":"Token revoked"}}.

StatusWhat CodeDock says
401 / 403Token invalid or revoked — enter it again
404Wrong base URL
429 / 5xxBackend temporarily unavailable, try again later
OtherThe message is shown as-is

Compared with established platforms

These fields were not invented from scratch — they are the greatest common divisor of the promo-code models of the mainstream payment platforms. If your codes already live in one of them, implementing this contract is mostly a field rename. If one Stripe Coupon mixes single-use and multi-use codes, split it into two offers — usage is the watershed for how CodeDock keeps the books.

CodeDockStripeLemon SqueezyPaddle Billing
offerCouponDiscountDiscount
codePromotion CodeDiscount’s codeDiscount’s code
usagederived from max_redemptionsderived from max_redemptionsderived from usage_limit
statusactivestatus (published / draft)status (active / archived)
max_redemptionsmax_redemptionsmax_redemptions (is_limited_redemptions)usage_limit
times_redeemedtimes_redeemedDiscount Redemptions counttimes_used
expires_atexpires_at / redeem_byexpires_atexpires_at

Versioning

The spec field and the /v1 in the path evolve together. Breaking changes go to /v2; v1 endpoints should keep working. Within v1, fields only get added: the client ignores unknown fields and unknown capabilities; an unknown usage makes it skip that offer, and an unknown status is treated as disabled — better to under-report than to report wrong.

Free and Pro

The free tier includes one product profile and ten contacts, and covers importing App Store codes, sending, reminders, and history. Connecting your own backend is part of Pro. For the full differences between free and Pro, see the FAQ.

FAQ