QVCCS innovation · How-to guides
How to build a secure Genesys Cloud callback request form for your website
A customer on your website wants to talk, but not now: at 17:30, after the school run. A callback request form gives them that choice and puts a scheduled callback on the right Genesys Cloud CX queue. This guide shows how we build one securely, with your own server between the browser and the Genesys Cloud Platform API.
Did you know Architect's Create Callback action, available in inbound, in-queue and outbound call flows, does not let callers schedule a callback for a specific date or time, whereas the Create Callback API accepts a callbackScheduledTime?
Did you know the key-value pairs you send in the data property of a Create Callback request appear in the attributes property on the conversation participant, ready for scripts and flows to use?
Did you know a client credentials token is not in the context of a user, so endpoints such as /api/v2/users/me are unavailable, and the OAuth client's access comes from the role and division pairs assigned to it?
Did you know that when the Genesys Cloud Platform API rate-limits a request it returns HTTP 429 with a Retry-After header, and the unit of that header is seconds?
01
A callback at a time that suits the customer
It is 12:40 and a customer is reading your returns policy on their phone during a lunch break. They have a question that needs a conversation, but they cannot talk until the evening, and they certainly do not want to wait in a queue. A callback request form on the website lets them say exactly that: here is my name, here is my number, call me at 17:30. The request becomes a callback conversation on a Genesys Cloud CX queue, scheduled for the time they chose, and an agent with the right skills takes it when the time comes.
Building that form looks simple, and the visible part is. The engineering that matters sits behind it. A Genesys Cloud OAuth client and its secret must never reach a browser, so the form cannot call Genesys Cloud directly. Every field arriving from the public internet must be treated as untrusted. And an endpoint that books outbound calls is attractive to anyone who wants to annoy your agents or someone else's phone. This guide walks through the design we use: the browser posts to your own server, your server validates the request and creates the callback through the Platform API.
02
What Genesys Cloud CX already offers for callbacks
Before writing any code, we check what the platform does natively, because the best integration is often the one you do not need. In Architect, the Create Callback action can be built into inbound, in-queue and outbound call flows. A caller waiting in queue can choose a callback rather than holding, and the action places a callback request on the queue, using the caller's ANI or another number collected in the flow, with an optional callback script whose inputs the flow supplies. Genesys documents one important limit: Architect does not currently allow callers to schedule a callback for a specific date or time.
Agents can also schedule callbacks during a voice interaction, choosing a date up to 30 days ahead within the window an administrator allows. For websites, Genesys publishes a schedule-callback sample widget on GitHub and points developers to the Create Callback API as the alternative. That API is what a self-service form needs: it lets a customer who is not on a call ask for a callback at a time of their choosing, on the queue you decide, with whatever context you want the agent to see. The rest of this guide builds on it.
03
The architecture: the browser talks only to your server
The design has three parts. The website serves a form with a handful of fields: name, callback number, preferred date and time, and an optional note. When the customer submits it, the browser sends only those fields over HTTPS to an endpoint on your own server. The server, which we usually build in Node.js with Express, is the only component that holds Genesys Cloud credentials. It checks the request, obtains an access token, and calls the Genesys Cloud Platform API in the region where your organisation lives. Nothing about the integration, not the client ID, not the secret, not a token, is ever visible in the page.
The first block of code sets up that server with the guard rails in place from the start. Cross-origin resource sharing (CORS) is restricted to your own website's origin rather than opened to every site, because an open configuration lets any web page drive your endpoint from a visitor's browser. Request bodies are capped at a few kilobytes. A rate limiter allows a small number of requests per client in a fifteen-minute window. Secrets and IDs are read from the environment, so they stay out of source control, and TLS is terminated in front of the application by your proxy or load balancer with a valid certificate.
// server.js: Node.js 18 or later (built-in fetch), Express 4, ES modules
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
const { env } = process; // client ID, secret and IDs come from the environment, never from code
const app = express();
app.set('trust proxy', 1); // TLS terminates at your proxy or load balancer
app.use(express.json({ limit: '10kb' })); // small JSON bodies only
// Browsers may call this endpoint only from your own website
app.use('/callback-request', cors({
origin: ['https://www.example.com'],
methods: ['POST'],
allowedHeaders: ['Content-Type']
}));
// At most five requests per client IP address every 15 minutes
app.use('/callback-request', rateLimit({
windowMs: 15 * 60 * 1000,
limit: 5,
standardHeaders: 'draft-7',
legacyHeaders: false
}));
app.listen(Number(env.PORT) || 3000);Read this diagram as text
Three panels from left to right show a website callback form, your own middleware server, and Genesys Cloud, with arrows for each request between them.
- The left panel, "Website · logged in – Callback form", shows fields for "Your name", "Callback number", "Preferred date and time" and "Notes", and a "Schedule callback" button.
- Under the form: "Posts the form fields only" and "No client ID, no secret, no token". An arrow labelled "HTTPS" runs from the button to the server panel.
- The middle panel, "Your server · middleware – Check, then call", lists "HTTPS endpoint with a TLS certificate", "Validate input · rate-limit each user" and "Build the callback request".
- A dashed box in the server panel, "Environment variables", holds "CLIENT_ID · CLIENT_SECRET", "Read on the server only, never sent to the browser". The panel notes "For example Node.js and Express".
- The right panel, "Genesys Cloud – Token, then callback", starts with "1 · Get a token": POST /oauth/token, "Client credentials grant → access token". An arrow runs from the server to this step and a return arrow runs back to the server.
- Next is "2 · Create the callback": POST /api/v2/conversations/callbacks, with "Queue · number · scheduled time · data". An arrow runs from the server's "Build the callback request" step to it.
- An arrow leads down from the callback step to an agent icon: "Offered to an agent at the scheduled time". The panel notes "Server-to-server calls only".
- A strip at the bottom reads: "The browser only ever talks to your server; only your server holds credentials and talks to Genesys Cloud."
04
An OAuth client that can do exactly one job
Your server authenticates with the OAuth client credentials grant, which Genesys describes as the grant for headless applications that need to make API requests without a user signing in. An administrator creates the client in Genesys Cloud under Menu > IT and Integrations > OAuth, selects Client Credentials, and assigns roles to it. We create a dedicated role for this one integration with the permission it needs, Conversation > Callback > Create, which is the conversation:callback:create permission the Create Callback endpoint requires, and we pair it with the division that holds the callback queue. Nothing else: no reporting, no user management, no recordings.
Treat the client secret like a password. Genesys has announced that client secrets are visible only when a client is created or its secret is reset, so the administrator copies it once into your secret store. Genesys has also announced optional allowed IP addresses for client credentials clients, strongly recommends configuring them, and expects them to become a requirement, so we register the server's outbound addresses. The token itself comes from POST https://login.<region>/oauth/token with the client ID and secret in a Basic Authorization header. The response includes expires_in, and the code caches the token until shortly before then, but Genesys warns that clients must not rely on expiry alone, so a 401 clears the cache and the call is retried once.
// GC_REGION is your org's region domain, e.g. mypurecloud.ie or euw2.pure.cloud
const LOGIN = `https://login.${env.GC_REGION}`;
const API = `https://api.${env.GC_REGION}`;
let cached = { token: null, expiresAt: 0 };
async function getToken() {
if (cached.token && Date.now() < cached.expiresAt) return cached.token;
const basic = Buffer.from(`${env.GC_CLIENT_ID}:${env.GC_CLIENT_SECRET}`).toString('base64');
const res = await fetch(`${LOGIN}/oauth/token`, {
method: 'POST',
headers: {
'Authorization': `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({ grant_type: 'client_credentials' })
});
if (!res.ok) throw new Error(`Token request failed with HTTP ${res.status}`);
const { access_token, expires_in } = await res.json();
// Renew five minutes early; a 401 also clears the cache (see createCallback)
cached = { token: access_token, expiresAt: Date.now() + Math.max(expires_in - 300, 60) * 1000 };
return access_token;
}Read this diagram as text
Two panels: an OAuth client set up in Genesys Cloud on the left, and the server where its values are kept on the right, joined by a "Copy" arrow.
- The left panel, "Genesys Cloud · IT and Integrations · OAuth – One OAuth client per integration", is headed by a client named "Website callbacks" using the "Client credentials grant".
- "Client ID": "Identifies the application", "Not a password on its own".
- "Client secret", highlighted as a caution: "Shown only on create or reset", "Treat it like a password".
- "Role · least privilege": "Only what the integration does, for example Conversation › Callback › Create".
- A dashed optional box: "Optional: allow token requests only from your server's IP addresses".
- An arrow labelled "Copy" runs from the client ID and secret to the right panel, "Your server – Where the values live", where a dashed "Environment file · server only" box holds "CLIENT_ID = ‹from the OAuth client›" and "CLIENT_SECRET = ‹never in code›".
- Below it, one ticked item reads "Used server-side to request an access token", and two crossed items read "Never in browser JavaScript or the web page" and "Never committed to source control".
- A strip at the bottom reads: "A client that can do one job, and a secret that never leaves the server – reset it if it ever might have."
05
Creating the callback with POST /api/v2/conversations/callbacks
The Create Callback request body is small, and every field earns its place. Routing comes from routingData.queueId, the queue that will offer the callback; the API also accepts a top-level queueId, and routingData can carry skills, a language and a priority if your routing needs them. callbackNumbers is the one required field, a list of numbers to call. callbackUserName tells the agent who asked. callbackScheduledTime is an ISO-8601 date-time, such as the output of JavaScript's toISOString(). scriptId optionally selects a Genesys Cloud script to show the agent, and data carries key-value pairs that appear as attributes on the conversation participant.
Validation happens on the server, never only in the browser. We require numbers in E.164 format, a plus sign followed by up to fifteen digits, and reject anything else with a helpful message; the API's validateCallbackNumbers flag can add a format check on the Genesys side as well. The name has a length limit, notes are truncated, and the scheduled time must be in the future and within the window the business has agreed to honour. Values that are fixed by design, the queue ID and script ID, come from configuration, never from the request, so a tampered form cannot route a callback anywhere else.
Errors are handled openly with the customer and in detail in the logs. The customer sees a short, friendly message; the server log records the detail, including the HTTP status from Genesys Cloud. A 429 response means a rate limit was reached, and its Retry-After header says how many seconds to wait; Genesys advises that further requests while limited only extend the limiting. For a form a customer is waiting on, we do not retry silently for long: we tell them to try again later and raise an alert if failures persist.
const E164 = /^\+[1-9]\d{6,14}$/; // a plus sign and up to 15 digits
const MAX_DAYS_AHEAD = 14; // your business rule
async function createCallback(body, retry = true) {
const res = await fetch(`${API}/api/v2/conversations/callbacks`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${await getToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (res.status === 401 && retry) { // token revoked or expired early
cached = { token: null, expiresAt: 0 };
return createCallback(body, false);
}
if (!res.ok) {
const wait = res.headers.get('Retry-After'); // seconds, sent with 429 responses
throw new Error(`Create callback failed with HTTP ${res.status}` + (wait ? `, retry after ${wait}s` : ''));
}
return res.json(); // { conversation: { id }, callbackIdentifiers: [...] }
}
app.post('/callback-request', async (req, res) => {
const { name, phone, when, notes, botToken } = req.body ?? {};
const scheduled = new Date(when);
const latest = Date.now() + MAX_DAYS_AHEAD * 24 * 60 * 60 * 1000;
if (typeof name !== 'string' || !name.trim() || name.length > 100)
return res.status(400).json({ error: 'Please enter your name.' });
if (typeof phone !== 'string' || !E164.test(phone))
return res.status(400).json({ error: 'Please enter your number in international format, for example +44 followed by your number.' });
if (Number.isNaN(scheduled.getTime()) || scheduled.getTime() < Date.now() || scheduled.getTime() > latest)
return res.status(400).json({ error: 'Please choose a time within the next two weeks.' });
try {
if (!(await verifyBotToken(botToken, req.ip)))
return res.status(400).json({ error: 'Please try again.' });
await createCallback({
routingData: { queueId: env.GC_CALLBACK_QUEUE_ID },
scriptId: env.GC_CALLBACK_SCRIPT_ID, // optional; omitted when not set
callbackUserName: name.trim(),
callbackNumbers: [phone],
callbackScheduledTime: scheduled.toISOString(),
data: { source: 'website-form', notes: String(notes ?? '').slice(0, 500) }
});
return res.status(201).json({ ok: true });
} catch (err) {
console.error(err.message); // log the detail, never return it
return res.status(502).json({ error: 'We could not book your callback. Please try again later.' });
}
});
async function verifyBotToken(token, ip) {
// Call your bot-protection provider's server-side verification here and
// return true only when it confirms the token for this request.
throw new Error('verifyBotToken is not implemented');
}06
The form, and the guard rails around it
The browser code is deliberately minimal. It gathers the form fields, strips spaces and brackets from the number, converts the date and time picker's local value to UTC, adds the token from your bot-protection widget, and posts JSON to your endpoint with fetch(). It shows the server's message in a status region that screen readers announce. It holds no secrets and knows nothing about Genesys Cloud. If the form and the endpoint share an origin, CORS is not involved at all; if the endpoint lives on another subdomain, as in this example, only your website's origin is allowed.
No single control stops abuse, so we layer them. CORS restricts which web pages a browser will let call the endpoint, but it does not stop scripts that are not browsers, which is why rate limiting and bot protection verified on the server matter. Where the form sits behind your customer sign-in, as many of our clients prefer, the server can also limit requests per customer account and add the customer's reference to the data property, giving the agent context and making misuse traceable. Every request is served over HTTPS, and the endpoint accepts only POST with a JSON body.
// On https://www.example.com: a form with inputs named name, phone, when
// (type="datetime-local"), notes and botToken, plus an element with role="status"
const form = document.querySelector('#callback-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const status = form.querySelector('[role="status"]');
const f = Object.fromEntries(new FormData(form));
const payload = {
name: f.name,
phone: String(f.phone).replace(/[\s()-]/g, ''), // +44 20 7946 0000 -> +442079460000
when: f.when ? new Date(f.when).toISOString() : '', // local time from the picker, sent as UTC
notes: f.notes,
botToken: f.botToken
};
try {
const res = await fetch('https://api.example.com/callback-request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const reply = await res.json();
status.textContent = res.ok ? 'Thank you. We will call you at the time you chose.' : reply.error;
} catch {
status.textContent = 'Something went wrong. Please try again later.';
}
});07
How we design, build and prove a callback integration
A callback form is a small piece of software that touches customer data, telephony and security, so we treat it with the same discipline as any integration. In Define, our Business Analyst captures who may request a callback, which queues serve which requests, the scheduling window and the non-functional requirements: volumes, availability and data protection. In Design, the Solution Architect records the endpoint contract in an Interface Control Document, from field formats to error responses, along with the OAuth client, the role, the division and the network allow-list. Senior Developers build to the Senior Platform Practice Lead's engineering standards, with four-eyes review of every change.
Proving it matters as much as building it. Our Systems Integration Tester works through the failure paths as deliberately as the happy path: an invalid number, a time in the past, a revoked secret, a token that expires mid-request, a 429 from the API, a bot token that fails verification, and a callback queue with no agents. UAT then confirms that agents see the name, the notes and the script they expect. In Run, monitoring and support follow your Genesys Cloud CX consumption and support model, whether we provide SLA-based support or work alongside your provider's service delivery model.
How it compares
Ways to offer a callback in Genesys Cloud CX
Native facts are as documented in the Genesys Cloud Resource Center and Developer Center, checked in October 2026.
| Aspect | Native Genesys Cloud CX options | Website form through the Platform API |
|---|---|---|
| Where the request starts | In a call: Architect's Create Callback action in inbound, in-queue or outbound call flows, or an agent scheduling a callback during a voice interaction. | On your website, from a customer who is not on a call, through POST /api/v2/conversations/callbacks. |
| Scheduled time | Architect does not let callers schedule a specific date or time; agents can schedule within the window an administrator allows. | The customer chooses; callbackScheduledTime carries it, within the window your business rules allow. |
| Number to call | The caller's ANI, or a number collected in the flow; agents can edit the number when scheduling. | The number the customer enters, validated as E.164 on your server. |
| Context for the agent | Callback script inputs set in the flow; attributes already on the conversation. | callbackUserName, an optional scriptId and data key-value pairs that appear as participant attributes. |
| Security responsibility | Handled within Genesys Cloud. | Yours: OAuth client secret custody, least-privilege role, CORS, validation, rate limiting and bot protection. |
| Build effort | Configuration in Architect and scripts. | A small server application to design, test, host and support, plus configuration. |
The approaches combine: many organisations offer in-queue callbacks to callers and a scheduled callback form to website visitors, both landing on the same queues.
The takeaways
- Customers choose when to be called, and the request lands on the right queue as a scheduled callback.
- Credentials stay on your server; the browser only ever sends the form fields.
- A dedicated OAuth client with one permission limits the impact of any mistake.
- Server-side validation, restricted CORS, rate limiting and bot protection work together.
- Agents see who asked and why, through the callback name, script and participant attributes.
Designed, built and supported by the same certified team that delivers Genesys Cloud CX solutions for enterprises and integrators.
Sources
- Schedule callbacks from a websitehelp.genesys.cloud
- Create Callback actionhelp.genesys.cloud
- Schedule a callbackhelp.genesys.cloud
- Callbacks overviewhelp.genesys.cloud
- Create an OAuth clienthelp.genesys.cloud
- OAuth client secret no longer visible after creationhelp.genesys.cloud
- Restrict OAuth client credentials grants to allowed IP addresseshelp.genesys.cloud
- Client credentials grant (Developer Center)developer.genesys.cloud
- Platform API regions and hosts (Developer Center)developer.genesys.cloud