Integrate AppClickPrint without missing a step
This is the primary implementation sequence for SaaS applications and AI development tools. Complete it in order. All AppClickPrint API calls belong in your trusted backend—never in browser or mobile frontend code.
The complete flow
- Create or obtain one AppClickPrint developer account.
- Obtain its one-time
acm_management key and store it in your backend secret store. - When one of your tenants enables printing, provision one AppClickPrint customer using your stable external customer ID.
- Store the returned one-time
acp_customer print key, encrypted at rest and associated with that external customer ID. - Add an Add Gateway button. Its frontend handler calls your backend, which requests a fresh gateway setup code and QR image from AppClickPrint.
- Show the QR code, manual setup-code fallback, expiry, Google Play link and concise connection instructions in your application.
- Record the customer’s current
gateway_count, then poll the customer endpoint every 2–5 seconds while the Add Gateway panel is open. - When
gateway_countincreases, retrieve the customer’s printers and update the UI. - Show available printers to the user. Store the selected immutable printer
idfor reconciliation and its currentaliasfor print submission. - Add a permanent Manage Gateways button. Each click requests a fresh portal session using
mode: "gateways"and opens its temporary URL in a new tab. - Submit jobs from your backend using that customer’s
acp_key and selected printer alias. - Persist the returned job
id. Treat HTTP202as queued—not as proof of printing.
Credential model
| Credential | Purpose | Allowed location |
|---|---|---|
acm_… | Developer management key. Provisions customers, creates portal/onboarding sessions, reads customer setup status, lists printers and manages webhooks. | Trusted backend secret store only |
acp_… | Customer print key. Submits jobs only for its isolated AppClickPrint customer. | Trusted backend, encrypted at rest, mapped to your external customer ID |
| Gateway setup code | Temporary eight-character code used only to register an Android, Windows or Linux gateway. Its QR image encodes this code and no credential. | May be shown to the authenticated intended customer; expires after 30 minutes |
access_… URL | Short-lived, one-use customer portal or onboarding access. | May be given to the intended customer; never log or reuse it |
acm_ or acp_ keys in JavaScript bundles, browser network calls, Android/iOS application code, URLs, analytics, logs or error messages. Your frontend calls your backend; your backend calls AppClickPrint.1. Create the developer account once
Create the account in the developer portal, or create it once from a trusted administrative backend:
POST https://appclickprint.com/v1/developers
Content-Type: application/json
{
"businessName": "Your SaaS",
"email": "[email protected]",
"password": "a-secure-portal-password",
"allowedOrigins": ["https://app.example.com"]
}
The 201 response includes managementKey. It is returned in plaintext at account creation, so store it immediately. The response also includes a ready-to-use Demo Customer. The developer’s first 100 successful prints are shared across all customers and do not require billing details.
2. Provision exactly one customer per tenant
Use your application’s permanent tenant/customer identifier as externalId. Do not use an email address, display name, browser session ID or a newly generated value.
POST https://appclickprint.com/v1/developer/customers
Authorization: Bearer acm_YOUR_MANAGEMENT_KEY
Idempotency-Key: provision:customer-4821:v1
Content-Type: application/json
{
"externalId": "customer-4821",
"businessName": "Example Company"
}
A newly created customer returns HTTP 201:
{
"created": true,
"customerId": "7c9b…",
"businessName": "Example Company",
"monthlyLimitMb": 50,
"adminEmail": null,
"apiKey": "acp_…",
"onboardingUrl": "https://appclickprint.com/access/access_…",
"setupCode": "ABCD-2345",
"expiresAt": "2026-08-14T03:30:00.000Z"
}
apiKey is the customer’s print key. Encrypt and store it immediately. If the same externalId already exists, the endpoint returns created: false and a fresh onboarding link, but it does not return or rotate the existing customer print key.
acp_ key requires deliberate key rotation. Do not depend on provisioning the same customer again to recover it.3. Implement the Add Gateway button
Keep an Add Gateway button available before and after initial setup so a customer can connect another Android device, Windows computer or Linux system at any time. The button calls your authenticated backend; your backend requests fresh setup details:
POST https://appclickprint.com/v1/developer/customers/customer-4821/gateway-code Authorization: Bearer acm_YOUR_MANAGEMENT_KEY Content-Type: application/json
HTTP/1.1 201 Created
{
"setupCode": "WXYZ-6789",
"qrCodeDataUrl": "data:image/png;base64,iVBORw0KGgo…",
"expiresAt": "2026-08-15T04:00:00.000Z",
"remainingUses": 3,
"playStoreUrl": "https://play.google.com/store/apps/details?id=com.appclickprint",
"windowsDownloadUrl": "https://appclickprint.com/downloads/AppClickPrint-Gateway-Windows-Setup-1.0.17.msi",
"windowsMsiUrl": "https://appclickprint.com/downloads/AppClickPrint-Gateway-Windows-Setup-1.0.17.msi",
"linuxDownloadUrl": "https://appclickprint.com/downloads/appclickprint-gateway_1.0.17_amd64.deb",
"desktopDeepLink": "appclickprint://setup?code=WXYZ-6789"
}
Return only these temporary display fields to the authenticated customer frontend. Prefer displaying qrCodeDataUrl as an image, with setupCode clearly visible and copyable. On Windows, windowsDownloadUrl and windowsMsiUrl identify the same stable installer and contain no setup credentials. Install or update the gateway with that MSI, open AppClickPrint Gateway, and enter setupCode manually before it expires—the same pattern used by Linux. The QR payload is exactly the setup code; do not generate a QR containing an acm_ key, acp_ key, customer ID or your own login token.
Show these instructions beside the QR code
- Install AppClickPrint Gateway from Google Play, or download the Windows/Linux desktop gateway, on a device that will remain able to reach the printers.
- Open the gateway and enter a meaningful name, such as “Reception”.
- On Android, scan the displayed QR. On Windows, install or update from
windowsDownloadUrl, open the gateway and enter the displayedsetupCode. The MSI filename is deliberately stable, so do not append the setup code or customer identity to it. On Linux, use the same manual-code flow. - Allow printer discovery. Android users should also follow the app’s battery/background-running recommendations so jobs continue arriving promptly.
- The code expires after 30 minutes and can register up to three gateways. Request a new code whenever the button is clicked; do not cache it.
- Requesting a code does not create another customer and does not rotate the stored
acp_key. - The older
mode: "onboarding"portal session remains available if you prefer AppClickPrint’s hosted, new-tab setup walkthrough.
4. Detect setup completion
There is currently no customer.setup_completed webhook. Before requesting the gateway code, read and remember gateway_count. While the Add Gateway panel is open, poll from your backend every 2–5 seconds:
GET https://appclickprint.com/v1/developer/customers/customer-4821 Authorization: Bearer acm_YOUR_MANAGEMENT_KEY
Read the exact setup_status field:
| Value | Exact meaning | Host application action |
|---|---|---|
waiting_for_gateway | No non-removed gateway is registered. | Keep polling while setup is open. |
discovering_printers | A gateway is registered, but no currently reported printer is available. | Tell the user to keep the gateway online and complete printer discovery. |
complete | At least one gateway is registered and at least one printer is present in discovery. | Stop polling and request the printer list. |
complete does not mean a test print succeeded, and it does not persist the user’s final “finished” button click. It means the integration can proceed to printer selection.
setup_status: "complete". After Add Gateway, wait for gateway_count to become greater than the count recorded before generating the code. Then retrieve printers again. Do not use a transition to complete as the only success condition.5. Support multiple gateways and ongoing management
An AppClickPrint customer is a site/account, not a single device or computer. A customer may register multiple Android, Windows or Linux gateways for printers in different areas, device replacement or operational resilience. Do not hide gateway access after the first printer is selected.
Your application must keep a visible Manage Gateways button in its printing settings. When the user clicks it, your frontend asks your trusted backend to create a fresh temporary session:
POST https://appclickprint.com/v1/developer/customers/customer-4821/portal-session
Authorization: Bearer acm_YOUR_MANAGEMENT_KEY
Content-Type: application/json
{ "mode": "gateways" }
HTTP/1.1 201 Created
{
"url": "https://appclickprint.com/access/access_…",
"expiresAt": "2026-08-14T05:00:00.000Z"
}
Open the returned url in a new browser tab. It signs the intended customer into gateway and printer management, where they can:
- register an additional Android, Windows or Linux gateway using a fresh QR/setup code;
- see printers grouped beneath each registered gateway;
- rename or rediscover a gateway;
- remove an old, lost or failed gateway; and
- configure printers and send non-billable AppClickPrint test pages.
mode: "gateways" session on every management click. Adding or removing gateways does not create another AppClickPrint customer and does not rotate the stored acp_ key.Required host-application behavior
- Use
POST …/gateway-codefor the permanent Add Gateway button before and after setup is complete. - Use
mode: "gateways"for the permanent Manage Gateways button before and after setup is complete. - After the management tab closes or the user returns, retrieve the printer list again before displaying or printing.
- If the selected printer was attached to a removed gateway, require the user to select an available printer again. A replacement gateway can create new printer IDs and aliases.
- Never put the
acm_key in the management link or call the portal-session endpoint directly from frontend code.
6. Retrieve and store the selected printer
GET https://appclickprint.com/v1/developer/customers/customer-4821/printers Authorization: Bearer acm_YOUR_MANAGEMENT_KEY
{
"setupStatus": "complete",
"printers": [{
"id": "2b9e…",
"name": "Reception Zebra",
"alias": "reception-zebra",
"driver": "zebra",
"transport": "network",
"available": true,
"gatewayId": "d104…",
"gatewayName": "Front Desk",
"printUrl": "https://appclickprint.com/v1/printers/reception-zebra/print"
}]
}
Identity and availability rules
idis immutable for that stored printer record. Keep it for reconciliation.aliasis the human-readable routing value used in the print URL. It is stable across normal reconnects on the same gateway, but a customer administrator can edit it.- If the printer is removed and rediscovered through another gateway, treat it as a new printer record with a new ID and possibly a new alias.
available: truemeans the printer was present in the gateway’s latest discovery report. It is not a guarantee that the gateway or printer is reachable at this instant.available: falsemeans the printer was absent from the latest discovery report. Print submissions to it are rejected rather than queued.gatewayIdidentifies one gateway registration. Removing and registering a replacement device creates a new ID.gatewayNameis a user-facing label and must not be treated as an identifier.
Store both the selected id and current alias in your application. Refresh the list when setup finishes, whenever the customer returns from Manage Gateways, after relevant webhooks, or when a stored alias returns “Available printer not found”.
7. Submit a print job from your backend
POST https://appclickprint.com/v1/printers/reception-zebra/print Authorization: Bearer acp_CUSTOMER_PRINT_KEY Content-Type: multipart/form-data format=pdf [email protected] filename=visitor-badge.pdf widthMm=62 heightMm=29 copies=1 photoMode=true idempotencyKey=visitor:94827:badge:v1
PDF page dimensions must match widthMm × heightMm, allowing the dimensions to be reversed for a rotated page. Provide exactly one document source: uploaded document or a gateway-downloadable documentUrl. The optional Boolean photoMode overrides the printer setting for this PDF job: true uses photograph-optimised dithering, while false favours crisp text and barcodes. Omit it to use the saved printer setting. It applies to compatible raster drivers; native IPP/AirPrint and operating-system drivers handle images themselves. For native printing, use format=zpl with data or padded dataBase64, or format=escpos with binary document or dataBase64. Raw jobs do not accept photoMode.
HTTP/1.1 202 Accepted
{
"id": "8f31…",
"status": "queued"
}
Persist id as your AppClickPrint job ID. A gateway that is temporarily offline can receive queued jobs when it reconnects, provided the printer record was still available when the job was submitted. If using documentUrl, keep the URL downloadable long enough for an offline gateway to reconnect.
Print-job lifecycle and current guarantees
Internal job states progress as follows:
queued → claimed → printing → completed
↘ failed
| State | Meaning |
|---|---|
queued | AppClickPrint accepted and stored the job. |
claimed | The assigned gateway claimed the job. |
printing | The gateway downloaded the content and started its driver operation. |
completed | The gateway reports that its print operation completed. Some printer protocols cannot prove that paper physically emerged. |
failed | The gateway or server reported failure; terminal webhook data includes the available plain-text error. |
GET /v1/print-jobs/{jobId} endpoint, and intermediate claimed/printing webhooks are not emitted. Use the returned job ID and terminal job.completed/job.failed webhooks. Do not represent HTTP 202 as “printed”.Webhooks available today
Create webhooks once with the acm_ management key. The endpoint must use HTTPS:
POST https://appclickprint.com/v1/developer/webhooks
Authorization: Bearer acm_YOUR_MANAGEMENT_KEY
Content-Type: application/json
{
"url": "https://api.example.com/webhooks/appclickprint",
"events": [
"job.completed",
"job.failed",
"gateway.online",
"gateway.offline",
"printer.discovered",
"printer.missing"
]
}
The 201 response returns a one-time secret beginning with whsec_. Store it securely. Every delivery uses this envelope:
{
"id": "unique-event-id",
"event": "job.completed",
"createdAt": "2026-08-14T04:12:00.000Z",
"data": { "jobId": "8f31…", "status": "completed", "error": null }
}
Verify X-AppClickPrint-Signature by computing HMAC-SHA256 over the raw request body with the webhook secret and comparing it with the header value sha256=<hex digest>. Deduplicate deliveries using the envelope id. Webhooks are developer-wide; correlate job, printer and gateway IDs with records already stored by your backend.
Supported events are job.completed, job.failed, gateway.online, gateway.offline, printer.discovered, printer.missing, usage.80, usage.95 and usage.limit_reached. There is currently no customer.setup_completed event; keep the onboarding poll described above.
Idempotency, retries and offline behavior
| Operation | Idempotency field | Scope and behavior |
|---|---|---|
| Provision customer | Idempotency-Key HTTP header | Scoped to the developer account. A repeat returns the original saved response with HTTP 200. Records currently have no published expiry, so never reuse a key for a different intended customer. |
| Submit print | idempotencyKey form/JSON field | Scoped to the AppClickPrint customer. A repeat returns the existing job ID and current stored status with HTTP 200. The key remains attached to the job. |
- On a connection timeout, HTTP 429 or HTTP 5xx, retry with the same idempotency key using exponential backoff and jitter.
- Do not retry HTTP 4xx unchanged. Correct authentication, validation, billing, limit or printer-selection errors first.
- The API does not currently return
Retry-Afteron 429 responses. Start around one second and back off to a sensible application limit. - A 10-second connection timeout and 60-second total request timeout are reasonable starting values; allow more for large uploads.
- If the gateway is offline but the printer remains marked available, the accepted job stays queued for reconnect.
- If
available=false, the print endpoint returns HTTP 404 and does not create a queued job.
Complete Node.js backend reference
This example deliberately contains no frontend AppClickPrint calls. Replace the three secretStore methods with your database/KMS implementation; values written there must be encrypted at rest.
// Node.js 20+ — trusted backend only
const ACP_BASE = "https://appclickprint.com";
const managementKey = process.env.APPCLICKPRINT_MANAGEMENT_KEY; // acm_...
// Implement with your encrypted database column or cloud secret/KMS service.
const secretStore = {
async putCustomerPrintKey(externalId, apiKey) { /* encrypt + upsert */ },
async getCustomerPrintKey(externalId) { /* read + decrypt */ },
async savePrinter(externalId, printer) { /* store printer.id + alias */ }
};
async function managementRequest(path, options = {}) {
const response = await fetch(`${ACP_BASE}${path}`, {
...options,
headers: {
Authorization: `Bearer ${managementKey}`,
...(options.body ? { "Content-Type": "application/json" } : {}),
...options.headers
},
signal: AbortSignal.timeout(30_000)
});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw Object.assign(new Error(body.message || body.error || "AppClickPrint request failed"), { status: response.status, body });
return body;
}
export async function provisionCustomer(externalId, businessName) {
const customer = await managementRequest("/v1/developer/customers", {
method: "POST",
headers: { "Idempotency-Key": `provision:${externalId}:v1` },
body: JSON.stringify({ externalId, businessName })
});
if (customer.apiKey) {
await secretStore.putCustomerPrintKey(externalId, customer.apiKey);
}
return { created: customer.created, customerId: customer.customerId };
}
// Call this from your authenticated backend route on every "Add Gateway" click.
// These temporary fields may be returned to that customer's frontend for display.
export async function createGatewayCode(externalId) {
const setup = await managementRequest(
`/v1/developer/customers/${encodeURIComponent(externalId)}/gateway-code`,
{ method: "POST" }
);
return {
setupCode: setup.setupCode,
qrCodeDataUrl: setup.qrCodeDataUrl,
expiresAt: setup.expiresAt,
remainingUses: setup.remainingUses,
playStoreUrl: setup.playStoreUrl
};
}
// Call this on every "Manage Gateways" click. Never cache its URL.
export async function createGatewayManagementSession(externalId) {
const session = await managementRequest(
`/v1/developer/customers/${encodeURIComponent(externalId)}/portal-session`,
{ method: "POST", body: JSON.stringify({ mode: "gateways" }) }
);
return { url: session.url, expiresAt: session.expiresAt };
}
export async function getSetupState(externalId) {
const id = encodeURIComponent(externalId);
const customer = await managementRequest(`/v1/developer/customers/${id}`);
if (customer.setup_status !== "complete") {
return { status: customer.setup_status, gatewayCount: Number(customer.gateway_count), printers: [] };
}
const result = await managementRequest(`/v1/developer/customers/${id}/printers`);
return { status: result.setupStatus, gatewayCount: Number(customer.gateway_count), printers: result.printers };
}
export async function selectPrinter(externalId, printer) {
if (!printer?.id || !printer?.alias || !printer.available) {
throw new Error("Select an available printer returned by AppClickPrint");
}
await secretStore.savePrinter(externalId, { id: printer.id, alias: printer.alias });
}
export async function submitPdf({ externalId, printerAlias, pdfBytes, widthMm, heightMm, sourceJobId, photoMode }) {
const customerKey = await secretStore.getCustomerPrintKey(externalId);
if (!customerKey) throw new Error("Customer print key is missing; rotate it deliberately rather than reprovisioning");
const form = new FormData();
form.set("format", "pdf");
form.set("document", new Blob([pdfBytes], { type: "application/pdf" }), "document.pdf");
form.set("widthMm", String(widthMm));
form.set("heightMm", String(heightMm));
form.set("copies", "1");
if (photoMode !== undefined) form.set("photoMode", String(photoMode));
form.set("idempotencyKey", `host-job:${sourceJobId}:v1`);
const response = await fetch(
`${ACP_BASE}/v1/printers/${encodeURIComponent(printerAlias)}/print`,
{
method: "POST",
headers: { Authorization: `Bearer ${customerKey}` },
body: form,
signal: AbortSignal.timeout(60_000)
}
);
const job = await response.json().catch(() => ({}));
if (!response.ok) throw Object.assign(new Error(job.message || job.error || "Print submission failed"), { status: response.status, body: job });
// HTTP 202 means queued. Persist job.id and wait for a terminal webhook.
return { jobId: job.id, status: job.status };
}
Billing and hard limits
Developers can provision customers during the shared 100-successful-print trial. After that, activate Stripe billing or arrange approved direct invoicing. Each customer site starts at $2.50 USD per month with a hard 50 MB monthly allowance; additional 50 MB blocks cost $1 USD. Handle trial_print_limit_reached, customer_limit_reached and monthly_print_limit_reached as explicit application states.
Up to 25,600 direct-URL jobs: documentUrl jobs use a fixed 2 KB of allowance. Uploaded and inline jobs have a 2 KB minimum and round up to the next 0.5 KB. Compact text-only uploads can exceed 20,000 labels at roughly 2.5 KB or less each. Read the metering and file-efficiency guide.