Developers
Everything the print-on-demand add-on registers, in one place. The tables marked as generated are written from the plugin's own source by the docs tooling, so they can't drift from the code.
The providers
| Code | Name | Driver | Synced import | Custom | Signed webhooks | Webhook setup |
|---|---|---|---|---|---|---|
printful |
Printful | Provider\Printful\PrintfulProvider |
Yes | Yes | Yes | By the add-on |
printify |
Printify | Provider\Printify\PrintifyProvider |
Yes | No | When Printify signs | By the add-on |
gelato |
Gelato | Provider\Gelato\GelatoProvider |
Templates, as custom links | Yes | No | By hand, in Gelato's dashboard |
kahunaprint |
Kahuna Print Co. | Provider\Demo\DemoProvider |
Yes | No | No webhooks | None needed |
Several can be connected at once. Each product is made by exactly one, so checkout never has to choose between them. The provider code is also the carrier code a shipping method is bound to. The demo provider's settings live under demo_provider in the config; its card and carrier code is kahunaprint, not demo, because demo is the shipping add-on's demo carrier and core keeps one carrier driver per code.
REST routes
The admin routes are documented one group to a page, with every parameter:
- Print on demand providers: the cards, the connection check, webhooks and shipping methods.
- Print on demand import: provider products, the import sheet, importing and refreshing.
- Print on demand custom products: blanks, Made by, links and print files.
- Print on demand bulk links: Assign by SKU, the links CSV and undo.
- Print on demand orders: the review queue and the provider orders.
Permissions
| Permission | Plugin | Label | Unlocks | Routes | Tools |
|---|---|---|---|---|---|
kahunacart.pod.manage |
Print on demand | Manage Print on Demand | Artwork, Catalogue, Imports, Link runs, Links, Providers, Shipping methods | 19 | 19 |
kahunacart.pod.orders |
Print on demand | Send Print on Demand Orders | Orders | 3 | 3 |
Sending is a permission of its own because it spends the store's money at the provider, the same reason core separates kahunacart.orders.refund from kahunacart.orders.manage. Reading provider orders, the review queue and Check again take core's kahunacart.orders.view.
MCP tools
Print on demand publishes 26 tools over 30 admin routes, behind 3 permissions. An assistant calls each one by its name below with kahunacart_pod_ in front of it.
These 4 routes are not published as tools: POST /kahunacart/pod/products/{id}/artwork, GET /kahunacart/pod/links/export, POST /kahunacart/pod/links/import, POST /kahunacart/pod/providers/{code}/webhook/address.
| Tool | Route | Permission | Arguments | What it does |
|---|---|---|---|---|
list_pod_providers |
GET /kahunacart/pod/providers |
kahunacart.pod.manage |
none | Every print-on-demand provider this store can use (Printful, Printify, Gelato, and the demo printer when it is on) with whether it is switched on, whether a key is pasted, whether orders go out for review first, and the capability matrix of what the driver can do |
check_pod_provider |
POST /kahunacart/pod/providers/{code}/check |
kahunacart.pod.manage |
code, required |
Make a real call to the provider with the pasted token and report whether it works: which store it reached, which scopes the token has, and which of the ones the add-on needs are missing |
register_pod_webhook |
POST /kahunacart/pod/providers/{code}/webhook |
kahunacart.pod.manage |
code required; reset optional |
Register this store's webhook address with the provider, so it says when an order is costed, sent, shipped, delivered, cancelled or put on hold, and when a blank goes out of stock |
update_pod_provider |
PATCH /kahunacart/pod/providers/{code} |
kahunacart.pod.manage |
code required; enabled, mode, auto_sync, manual_approval_confirmed optional |
Change the switches on a provider's card: switch it on or off, choose whether paid orders wait for review or are sent automatically, and whether products made at the provider come in by themselves as drafts |
add_pod_shipping_methods |
POST /kahunacart/pod/providers/{code}/shipping-methods |
kahunacart.pod.manage |
code, zone_ids required; services optional |
Add the provider's shipping methods to shipping zones |
list_provider_products |
GET /kahunacart/pod/providers/{code}/products |
kahunacart.pod.manage |
code required; offset, limit optional |
The products the merchant has designed at the provider, a page at a time, each with its thumbnail, how many variants it has, whether it is switched off at the provider, and the KahunaCart product id it was imported as, or null when it is not in the store yet |
get_provider_product |
GET /kahunacart/pod/providers/{code}/products/{ref} |
kahunacart.pod.manage |
code, ref required; markup optional |
One of the merchant's products at the provider, with every variant — its colour and size, SKU, cost in the provider's billing currency, retail price, and whether it can be made — and the import sheet beside it: a suggested selling price per variant in the store's currency, from the provider's retail price or from cost plus the markup |
import_pod_product |
POST /kahunacart/pod/imports |
kahunacart.pod.manage |
provider, ref required; prices, accept_suggested, markup_percent optional |
Import a provider's product as a draft KahunaCart product of type Print on demand, with its options, one variant per variant switched on at the provider, its pictures and a link per variant |
refresh_pod_product |
POST /kahunacart/pod/imports/{id}/refresh |
kahunacart.pod.manage |
id required; title, description, images, prices optional |
Read an imported product from the provider again and bring in its facts: cost, availability, SKU and weight on every variant still there, new variants added switched off for the merchant to price and switch on, and variants the provider dropped marked unavailable but kept |
get_pod_links |
GET /kahunacart/pod/products/{id}/links |
kahunacart.pod.manage |
id, required |
Which provider makes each variant of a product, with the provider's references, the cost, whether the variant can be sold, and the reason when it cannot |
set_pod_links |
PUT /kahunacart/pod/products/{id}/links |
kahunacart.pod.manage |
id, links, all required |
Switch variants of a product made by a provider on or off for sale |
search_pod_catalogue |
GET /kahunacart/pod/providers/{code}/catalogue |
kahunacart.pod.manage |
code required; q, offset, limit optional |
Search a provider's catalogue of blank products for a custom product: a Printful tee, hoodie or poster with every colour and size it comes in |
get_pod_blank |
GET /kahunacart/pod/providers/{code}/catalogue/{blank} |
kahunacart.pod.manage |
code, blank, all required |
One catalogue product with every variant: its colour and size, the blank id a custom link stores, its cost in the provider's billing currency, whether it can be made now, and the print areas it takes with the print-file size each wants |
set_pod_made_by |
PUT /kahunacart/pod/products/{id}/made-by |
kahunacart.pod.manage |
id required; provider, mode, match_from, blanks, dry_run optional |
Set which provider makes a product, and from which blank each of its variants is made |
list_pod_artwork |
GET /kahunacart/pod/products/{id}/artwork |
kahunacart.pod.manage |
id, required |
A custom product's print files: which print area and which variant each is for (none means every variant), its type, pixel size and the public address the provider fetches it from, with a warning when it would print under 150 dpi; and the print areas each variant still needs a file for |
delete_pod_artwork |
DELETE /kahunacart/pod/artwork/{id} |
kahunacart.pod.manage |
id, required |
Delete a print file |
assign_pod_links_by_sku |
POST /kahunacart/pod/links/assign |
kahunacart.pod.manage |
match, provider required; mode, blank, files, dry_run optional |
Link every variant whose SKU a rule catches to a provider: a SKU prefix, or a numeric range compared on the SKU's leading digits |
list_pod_link_runs |
GET /kahunacart/pod/link-runs |
kahunacart.pod.manage |
offset, limit |
The bulk runs that assigned providers — SKU rules and links CSV imports — newest first, with what each did, the lines a CSV refused, and whether it has been undone |
undo_pod_link_run |
POST /kahunacart/pod/link-runs/{id}/undo |
kahunacart.pod.manage |
id, required |
Undo a bulk run: remove the links it wrote, put back the links it replaced, take away the print files it added, and put back any product it left with no links as it was |
get_pod_review_queue |
GET /kahunacart/pod/review |
kahunacart.orders.view |
limit |
Everything print on demand is waiting on a person for, with counts: provider orders that need attention (with the provider's own words), orders waiting to be sent with their cost, products auto-sync brought in as drafts, published products whose cost went up, and products with a variant no longer made or a print file missing |
list_pod_orders |
GET /kahunacart/pod/orders |
kahunacart.orders.view |
status, provider, limit, offset |
Every provider order — one per provider per KahunaCart order — with its KahunaCart order number, provider, status, the provider's own status word, cost in the provider's billing currency, what the customer paid for those lines, the margin when both are in one currency, and which buttons apply |
get_order_pod |
GET /kahunacart/orders/{id}/pod |
kahunacart.orders.view |
id, required |
The provider orders of one KahunaCart order: status, cost once the provider has worked it out, margin, the provider's words when something is wrong, its dashboard link, the lines sent, the parcels with their tracking, and which buttons apply |
send_pod_order |
POST /kahunacart/pod/orders/{id}/send |
kahunacart.pod.orders |
id, required |
Send a provider order waiting in review to production |
resend_pod_order |
POST /kahunacart/pod/orders/{id}/resend |
kahunacart.pod.orders |
id, required |
Send again a provider order that needs attention, once its cause is fixed |
refresh_pod_order |
POST /kahunacart/pod/orders/{id}/refresh |
kahunacart.orders.view |
id, required |
Read a provider order back from the provider now and record what it says: status, cost, the provider's words, new parcels with their tracking, deliveries |
cancel_pod_order |
POST /kahunacart/pod/orders/{id}/cancel |
kahunacart.pod.orders |
id, required |
Cancel a provider order at the provider: a draft is deleted, an order not yet started is cancelled and its cost goes back to the provider wallet |
Public routes
Unauthenticated. {api} is the API plugin's base, /api/v1 by default.
| Route | When it is registered | What it is |
|---|---|---|
GET {api}/kahunacart/pod/public/artwork/{token} |
always | Where a provider fetches a print file. The token in the path is the credential, and it changes whenever the file is replaced. |
HEAD {api}/kahunacart/pod/public/artwork/{token} |
always | The same file's headers, for a provider that checks before it fetches. |
POST {api}/kahunacart/pod/public/webhook/{provider}/{secret} |
always | Where a provider posts its webhooks. Checked for size (413), the secret (404) and, where the provider signs, the signature (401), then logged and answered with no call back to the provider. See Tracking. |
GET {api}/kahunacart/pod/public/demo-track/{number} |
only while the demo provider is on | The demo printer's tracking page. See The demo provider. |
CLI
| Command | What it does |
|---|---|
bin/plugin kahunacart-print-on-demand check |
Check a print-on-demand provider's token with a real call: the store it reaches and its scopes |
bin/plugin kahunacart-print-on-demand demo:tick |
Move every open Kahuna Print Co |
bin/plugin kahunacart-print-on-demand import |
Import a product from a print-on-demand provider as a draft, priced from the import sheet |
bin/plugin kahunacart-print-on-demand links:export |
Write every print-on-demand link as a links CSV |
bin/plugin kahunacart-print-on-demand links:import |
Assign providers to variants from a links CSV (sku, provider, mode, blank, file: |
bin/plugin kahunacart-print-on-demand links:undo |
Undo a bulk run of print-on-demand links (a SKU rule or a links CSV) |
bin/plugin kahunacart-print-on-demand orders |
List print-on-demand orders, read them back from the provider, or cancel one |
bin/plugin kahunacart-print-on-demand products |
List the products designed at a print-on-demand provider, and which are already imported |
bin/plugin kahunacart-print-on-demand record |
Record the test fixtures from a real Printful store, Gelato account or Printify shop (reads only, never orders), scrubbed |
bin/plugin kahunacart-print-on-demand seed |
Fill the store with the demo printer's products, shipping methods and two orders (idempotent; --purge removes them again) |
bin/plugin kahunacart-print-on-demand shipping |
Add a print-on-demand provider's shipping methods to shipping zones |
bin/plugin kahunacart-print-on-demand webhook |
Register this store's webhook with a print-on-demand provider, or for Gelato print the address and the steps to add it by hand |
No command sends an order to production.
Jobs
Handed to KahunaCart's job queue. Every one is safe to run twice.
| Type | When | What it does |
|---|---|---|
pod.submit_order |
When an order counts as paid, after a change, on Send again | Looks the order up by reference, makes it at the provider if it is not there, and in auto mode sends it once the cost is known (waiting a minute at a time, up to six hours). |
pod.sync_order |
After a webhook, from the poll, after a submit, on Check again | Reads one provider order back and records its status, cost, words and parcels. One per order at a time. |
pod.cancel_order |
Cancel at …, a cancelled or fully refunded KahunaCart order | Cancels one provider order, or records that the provider would not. |
pod.poll_orders |
From the worker tick | Queues a read of every open order older than tracking.poll_hours, 25 at a time, and sends the daily waiting and token reminders. |
pod.sync_catalog |
Daily, and on Printful's stock event | Availability and cost for every link, and Gelato's stock for products that can run out. |
pod.auto_sync |
A product webhook with auto-sync on | Imports or refreshes one product as a draft, and answers Printify's Publish button. |
pod.notify_synced |
The end of an hour with auto-synced drafts | The hour's one pod.products_synced notification. |
pod.links_import |
A links CSV committed from the admin | Downloads each file once and writes the good rows as one run. |
pod.shop_disconnected |
Printify's shop:disconnected |
Switches Printify off and tells you, when it is the shop this store sends to. |
pod.sweep_webhooks |
Daily | Deletes webhook log rows older than thirty days. |
Notifications
Sent on the channels you choose in KahunaCart's Notifications settings.
| Code | Priority | When |
|---|---|---|
pod.needs_attention |
high | A provider could not go ahead with an order, or it could not be sent. Carries the provider's words. |
pod.waiting_review |
normal | Once a day while orders have waited for Send longer than notify.waiting_after_hours. |
pod.cancel_refused |
normal | A cancelled order had already been started by the provider, so its cost stands. |
pod.variant_unavailable |
normal | Once a day, how many options a provider no longer makes since the last check. |
pod.products_synced |
normal | Auto-synced drafts arrived. One an hour at most. |
pod.cost_increased |
normal | A product you sell costs more to make than it did. |
pod.negative_margin |
normal | A provider's cost for an order is more than the customer paid for those items and their shipping. |
pod.token_expiring |
normal | Daily in the fortnight before a token's expiry date. |
The review queue also has a dashboard alert, pod_queue, at the top of every KahunaCart admin screen. On KahunaCart 1.5.3 or later it also has a row of its own under Notifications → Store owner (alert.pod_queue, normal): one message when the queue first has something in it and a "Cleared" one when it is empty again.
A delivery to the webhook address that is turned away because the secret in the address is wrong or missing, or because the signature does not check out, is also sent to KahunaCart's Webhook rejected row (KahunaCart 1.5.3 or later), under the provider's name and the reason bad_secret, no_secret_configured or bad_signature. It is one message a day per provider and reason however many deliveries are refused. A slug that is not one of this store's providers is not reported. See Report a webhook your own endpoint refused.
Tables
| Table | What it holds |
|---|---|
kahunacart_pod_links |
One row per variant made by a provider: the provider, synced or custom, the provider's references, the cost, whether it can be sold and why not. |
kahunacart_pod_imports |
One row per imported provider product, for Refresh. |
kahunacart_pod_artwork |
Print files: the product, the variant (none for every variant), the print area, the file, its token, and when it was replaced. |
kahunacart_pod_link_runs |
Bulk runs (SKU rules and links CSVs) with what each wrote and replaced, for Undo. |
kahunacart_pod_orders |
Provider orders: one per provider per order attempt, with the status, the provider's id and words, the cost, what was last sent, and the claim that keeps two actions from running at once. |
kahunacart_pod_shipments |
Parcels, each tied to its core shipment (0 for a replacement parcel). |
kahunacart_pod_webhooks |
The webhook delivery log, kept thirty days. |
Migrations pod_0001_core to pod_0004_order_claims, run by KahunaCart's migrator. kahunacart reset empties the order and parcel tables with the sales.
Events
The add-on fires no event of its own. It listens to these core events:
onKahunaCartAccountPanelslistensAdd a panel to an account page
Payload registry, section, user, customer, account_base
onKahunaCartAdminSectionslistensContribute an admin screen (§8)
Payload sections
onKahunaCartCartLineValidatelistensRefuse a cart line, or refuse what it is joining
Payload order_id, product, variant, qty, line_data, item_id, cart_items, errors
onKahunaCartCheckoutValidatelistensRefuse an order before payment starts
Payload order, items, post, context (checkout, api or express), errors
onKahunaCartCollectEncryptedSettingslistensHave a setting of your own stored encrypted without being a payment provider
Payload keys (a list of plugins.<plugin>.<key> paths; append yours and write the list back)
onKahunaCartDeclareFeatureCompatibilitylistensDeclare which core features your add-on understands
Payload features, codes
onKahunaCartOrderChangeConfirmedlistensA change was applied to a new version
Payload order_id, change_id, change, version_from, version_to, order, restocked
onKahunaCartOrderPaidlistensMoney landed after the fact
Payload order_id
onKahunaCartOrderPanelslistensAdd a panel to a customer's receipt page and to their order emails
Payload registry, order, channel, site_url
onKahunaCartProductSavedlistensA product was written — created, edited, published, unpublished, hidden, or one of its variants changed
Payload id, action (created, updated or variant)
onKahunaCartRefundAppliedlistensA refund the provider confirmed has been applied to the goods
Payload order_id, order, transaction, items, shipping_minor, full
onKahunaCartRegisterCarrierRateProviderslistensPlug a live carrier into shipping
Payload registry (a Shipping\Carrier\CarrierRegistry; call add() on it)
onKahunaCartRegisterDashboardAlertslistensTell a merchant about something in your add-on that needs their attention, on the dashboard rather than in an email
Payload registry (an Admin\Alerts\AlertRegistry to call register() on)
onKahunaCartRegisterHookslistensRun a side effect inside an order flow with an undo that runs automatically, in reverse, when a later step fails
Payload registry (an Extension\Hooks\HookRegistry to call on() on)
onKahunaCartRegisterJobHandlerslistensRegister a background job type of your own
Payload registry
onKahunaCartRegisterMigrationslistensMigrations are being collected. Append your own migrations directory
Payload paths
onKahunaCartRegisterNotificationslistensRegister the emails your plugin sends so they appear on the Notifications screen with a switch and a template picker
Payload registry
onKahunaCartRegisterProductTypeslistensRegister a kind of thing this store can sell
Payload registry
onKahunaCartRegisterResetTableslistensDeclare your add-on's transactional tables so a pre-launch reset takes them too, and leave your configuration tables out of it
Payload registry (an Ops\ResetTableRegistry to call add() or addAll() on)
onKahunaCartRegisterSettingsNamespaceslistensDeclare a namespace of scoped settings for your add-on, stored in kahunacart_settings with no columns of your own
Payload registry (a Extension\SettingsNamespaceRegistry to call register() on)
onKahunaCartRegisterZoneslistensDeclare a zone on a screen of your own so a fourth party can extend it, or mount a widget into a catalogued zone
Payload registry (a Extension\Zone\ZoneRegistry to call declare() and mount() on)
onKahunaCartShippingMethodExcludedlistensReinstate a refused shipping method, reword why it was refused, or attach carrier metadata
Payload method, calculation, package, active, reason, reasonKey, reasonParams, metadata, reinstate
onKahunaCartWorkerTicklistensPut recurring work of your own on the store's own worker clock
Payload queue, now