Task Status — Respond Form
The /respond endpoint renders an intermediate page when a customer clicks an appointment link in an email. Instead of processing the action immediately via a GET request, the customer sees a form that summarises the appointment and confirms their choice before submitting. This prevents accidental confirmations or rejections triggered by email pre-fetchers or link scanners.
| Request | |
|---|---|
| Description | Renders an HTML form that lets the customer confirm or reject a scheduled appointment. |
| URL | /rs/v1/customer/task/respond |
| Method | GET |
| Response Type | text/html |
| Parameter | Datatype | Description |
|---|---|---|
| configId | Integer | The identifier of the configuration. |
| taskId | String | The external identifier of the task. |
| status | String | The target status code. |
| action | String | Either confirm or reject. |
| authentication | String | The authentication token from the email link (UUID). |
When the customer submits the form, a fetch POST is sent to /rs/v1/customer/task/confirm or /rs/v1/customer/task/reject depending on the action. The response is a bare HTTP status code with no body; the template JavaScript maps each status code to a user-facing message.
Both POST endpoints accept the following form-encoded parameters:
| Form Parameter | Required | Description |
|---|---|---|
configId |
Yes | The identifier of the configuration. |
taskId |
Yes | The external identifier of the task. |
status |
Yes | The target status code. |
authentication |
Yes | Authentication token UUID from the email link. |
message |
No | Free-text message from the customer. Stored as a task note and triggers a notification email. |
POST response codes
| Endpoint | Code | Meaning |
|---|---|---|
confirm |
200 | Appointment confirmed successfully. |
confirm |
409 | Appointment was already confirmed. |
confirm |
500 | Server error. |
reject |
200 | Rejection registered successfully. |
reject |
500 | Server error. |
Customising the Respond Form
By default the /respond endpoint renders a built-in, unstyled form. You can replace this with a fully custom HTML page per configuration by creating a Template entity named appointmentRespondForm.
The same template is used for both confirm and reject actions. The {{action}} placeholder resolves to either confirm or reject at render time, allowing the template to pre-select the correct block on load.
The template must be a complete HTML document. The following placeholders are substituted at render time:
| Placeholder | Replaced with |
|---|---|
{{action}} |
Either confirm or reject (from the email link) |
{{task}} |
URL-encoded external task identifier |
{{scheduled}} |
Scheduled date/time in yyyy-MM-dd HH:mm:ss format |
{{configId}} |
Configuration identifier |
{{taskId}} |
External task identifier (not URL-encoded) |
{{authToken}} |
Authentication token UUID from the email link |
{{company.name}} |
Company name from the company.name configuration preference |
Response handling
The POST endpoints return only an HTTP status code — no response body. The template JavaScript is responsible for mapping each status code to a user-facing message and displaying it. This keeps all customer-facing text inside the template, where it can be translated and branded without touching server code.
The recommended pattern is a MESSAGES object keyed by action and status code:
var MESSAGES = {
confirm: {
200: { ok: true, text: 'Your appointment has been confirmed!' },
409: { ok: false, text: 'This appointment has already been confirmed.' },
500: { ok: false, text: 'An error occurred. Please contact us directly.' }
},
reject: {
200: { ok: true, text: 'Your cancellation has been received.' },
500: { ok: false, text: 'An error occurred. Please contact us directly.' }
}
};
Example — Custom Respond Form
The example below handles both confirm and reject in a single template. Two top-level buttons switch the visible content block. Each block has its own descriptive text, an optional textarea, and a submit button. The block matching {{action}} is pre-selected on page load.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Appointment Response</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 0; }
.container { max-width: 580px; margin: 60px auto; background: #fff;
border-radius: 8px; padding: 40px;
box-shadow: 0 2px 8px rgba(0,0,0,0.12); }
h1 { font-size: 1.4rem; color: #333; }
p { color: #555; line-height: 1.6; }
label { display: block; margin-bottom: 6px; color: #555; font-weight: bold; }
textarea { width: 100%; box-sizing: border-box; padding: 8px;
border: 1px solid #ccc; border-radius: 4px;
font-size: 0.95rem; margin-bottom: 20px; }
.actions { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px; }
.btn { padding: 12px 28px; border: 2px solid transparent; border-radius: 5px;
font-size: 1rem; cursor: pointer; color: #fff; }
.btn-confirm { background: #2e7d32; }
.btn-confirm:hover { background: #1b5e20; }
.btn-reject { background: #c62828; }
.btn-reject:hover { background: #7f0000; }
.btn-selected { border-color: #000; box-shadow: 0 0 0 3px rgba(0,0,0,0.25); }
.action-block { display: none; }
.footer { margin-top: 28px; color: #777; font-size: 0.9rem; }
#result { display: none; margin-top: 16px; padding: 16px 20px;
border-radius: 4px; font-size: 1rem; line-height: 1.5; }
.result-ok { background: #e8f5e9; color: #1b5e20; border: 1px solid #a5d6a7; }
.result-error { background: #ffebee; color: #b71c1c; border: 1px solid #ef9a9a; }
</style>
</head>
<body>
<div class="container">
<h1>Appointment Response</h1>
<p>Task: {{task}}</p>
<p>Scheduled: <span id="scheduled">{{scheduled}}</span></p>
<form method="post" action="#" id="respondForm">
<input type="hidden" name="configId" value="{{configId}}">
<input type="hidden" name="taskId" value="{{taskId}}">
<input type="hidden" name="status" id="statusField" value="">
<input type="hidden" name="authentication" value="{{authToken}}">
<input type="hidden" name="message" id="messageField" value="">
<div class="actions">
<button type="button" class="btn btn-confirm" data-action="confirm">Confirm appointment</button>
<button type="button" class="btn btn-reject" data-action="reject">Decline appointment</button>
</div>
<!-- Confirm block -->
<div id="block-confirm" class="action-block">
<p>We are glad the appointment works for you.</p>
<label for="msg-confirm">Message to us (optional):</label>
<textarea id="msg-confirm" rows="4"
placeholder="Any special requirements?"></textarea>
<button type="button" class="btn btn-confirm"
data-status="IP-C" data-endpoint="confirm">Confirm</button>
</div>
<!-- Reject block -->
<div id="block-reject" class="action-block">
<p>We are sorry the proposed time does not work.</p>
<label for="msg-reject">Your preferred time slots (optional):</label>
<textarea id="msg-reject" rows="4"
placeholder="Let us know which time slots work for you…"></textarea>
<button type="button" class="btn btn-reject"
data-status="IP-R" data-endpoint="reject">Decline</button>
</div>
</form>
<div id="result"></div>
<p class="footer">Thank you for your response.<br>{{company.name}}</p>
</div>
<script>
function selectAction(action) {
document.querySelectorAll('.btn[data-action]').forEach(function (b) {
b.classList.toggle('btn-selected', b.dataset.action === action);
});
document.querySelectorAll('.action-block').forEach(function (el) {
el.style.display = el.id === 'block-' + action ? 'block' : 'none';
});
}
var MESSAGES = {
confirm: {
200: { ok: true, text: 'Your appointment has been confirmed!' },
409: { ok: false, text: 'This appointment has already been confirmed.' },
500: { ok: false, text: 'An error occurred. Please contact us directly.' }
},
reject: {
200: { ok: true, text: 'Your cancellation has been received.' },
500: { ok: false, text: 'An error occurred. Please contact us directly.' }
}
};
function showResult(success, text) {
var resultDiv = document.getElementById('result');
resultDiv.className = success ? 'result-ok' : 'result-error';
resultDiv.textContent = text;
resultDiv.style.display = 'block';
document.getElementById('respondForm').style.display = 'none';
}
function respond(status, endpoint) {
var isConfirm = (status === 'IP-C');
var action = isConfirm ? 'confirm' : 'reject';
var activeTextarea = document.querySelector(
'#block-' + (isConfirm ? 'confirm' : 'reject') + ' textarea');
var form = document.getElementById('respondForm');
var params = new URLSearchParams();
params.set('configId', form.querySelector('[name="configId"]').value);
params.set('taskId', form.querySelector('[name="taskId"]').value);
params.set('status', status);
params.set('authentication', form.querySelector('[name="authentication"]').value);
params.set('message', activeTextarea ? activeTextarea.value : '');
form.querySelectorAll('.btn[data-status]').forEach(function (b) { b.disabled = true; });
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
credentials: 'include',
body: params.toString()
})
.then(function (res) {
var map = MESSAGES[action] || {};
var entry = map[res.status] || map[500] || { ok: false, text: 'An unknown error occurred.' };
showResult(entry.ok, entry.text);
})
.catch(function () {
showResult(false, 'The connection to the server failed. Please try again later.');
form.querySelectorAll('.btn[data-status]').forEach(function (b) { b.disabled = false; });
});
}
(function init() {
var el = document.getElementById('scheduled');
var text = el.textContent.trim();
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(text)) {
el.textContent = new Date(text).toLocaleString();
}
document.querySelectorAll('.btn[data-action]').forEach(function (btn) {
btn.addEventListener('click', function () { selectAction(btn.dataset.action); });
});
document.querySelectorAll('.btn[data-status]').forEach(function (btn) {
btn.addEventListener('click', function () { respond(btn.dataset.status, btn.dataset.endpoint); });
});
selectAction('{{action}}');
})();
</script>
</body>
</html>
The status codes IP-C (confirm) and IP-R (reject) in the data-status attributes are examples. Replace them with the actual status codes configured in your OMD instance.
All user-facing messages live entirely inside the template. The POST endpoints return only an HTTP status code — no response body. Translate or adjust the MESSAGES object to match the language and tone of your customer communication.
Sample Link (generated by the email template)
https://<instance>.optimizemyday.com/omdservices/rs/v1/customer/task/respond
?configId=16168276
&taskId=AU-2025-71146_Weissach-282
&status=IP-C
&action=confirm
&authentication=<token-uuid>
Expired Token Page
Authentication tokens embedded in email links have a limited validity period. When a customer opens a link after the token has expired, they are automatically redirected to the /expired endpoint, which displays a user-friendly message instead of the login screen.
| Request | |
|---|---|
| Description | Public page shown when the authentication token in the email link has expired. |
| URL | /rs/v1/customer/task/expired |
| Method | GET |
| Response Type | text/html |
| Authentication | None required — publicly accessible. |
| Parameter | Datatype | Description |
|---|---|---|
| configId | Integer | The identifier of the configuration. |
Default Message
If no custom template is configured, the page renders a built-in styled message:
This link has expired
The link you used is no longer valid. Please contact us if you still need to confirm or change your appointment.
Customising the Expired Token Page
The default message can be overridden per configuration by creating a Template entity named exactly expiredTokenTemplate.
- In the OMD administration interface, navigate to Configuration → Templates.
- Create a new template with:
- Name:
expiredTokenTemplate - Value: a complete HTML document (see example below).
- Save. The custom template is picked up immediately on the next request — no restart required.
Example Custom Template
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Link expired</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; padding: 0; }
.container {
max-width: 540px; margin: 60px auto; background: #fff;
border-radius: 8px; padding: 40px;
box-shadow: 0 2px 8px rgba(0,0,0,0.12); text-align: center;
}
h1 { font-size: 1.4rem; color: #333; margin-bottom: 16px; }
p { color: #555; line-height: 1.6; }
a { color: #1565c0; }
</style>
</head>
<body>
<div class="container">
<h1>This link has expired</h1>
<p>
The link you used is no longer valid.<br>
Please contact us at
<a href="mailto:service@example.com">service@example.com</a>
if you still need to confirm or change your appointment.
</p>
</div>
</body>
</html>
The template value must be a complete HTML document (including <!DOCTYPE html>, <html>, <head> and <body> tags). Partial HTML fragments are not supported.