If you've ever built appointment reminders, you know the moment. The confirmation email is scheduled — queued for 2:00 PM, sitting quietly in the API. Then at 1:15 PM the patient calls: their meeting ran long, can we push the appointment to 4:00? Now your perfectly scheduled email is wrong, and the obvious fix — cancel it and create a new one — is quietly dangerous. You lose the message ID, you re-run your send logic, and if anything goes wrong in between, you've either spammed the patient or sent nothing at all.
This post walks through email-schedule-rescheduler, a Python sample that treats rescheduling as a first-class operation instead of a workaround. It schedules an email with a future scheduled_at timestamp, moves that timestamp forward with a single PATCH call, verifies the change with a GET, and demonstrates that the Telnyx Email API rejects reschedule attempts into the past with a 422 error. In workflows like patient communications, silently sending an email that was meant to go out hours ago isn't a bug you can afford. The API agrees.
What the App Does
The sample is a single Python script that runs four demo steps in sequence, then cleans up after itself:
- Schedule an email with a future
scheduled_attimestamp (POST /v2/email_messages→202 Acceptedwith a message ID). - Reschedule it to a new future time (
PATCH /v2/email_messages/{id}/schedule). - Attempt an invalid reschedule to a timestamp in the past — and confirm the API rejects it with
422. - Verify the new delivery time by retrieving the message (
GET /v2/email_messages/{id}).
Finally, it cancels the scheduled email (DELETE /v2/email_messages/{id}/schedule) so the demo doesn't leave anything behind.
By default the script runs in demo mode (DEMO_MODE=true): it logs every request it would make without touching the API. Set DEMO_MODE=false with a real API key and it runs the identical flow live. That makes it safe to clone, run, and read before you ever spend a send.
How It Works
Four endpoints carry the whole flow:
| Method | Endpoint | Purpose |
|---|---|---|
POST | /v2/email_messages | Create a scheduled email with a future scheduled_at |
PATCH | /v2/email_messages/{id}/schedule | Reschedule to a new future time |
GET | /v2/email_messages/{id} | Retrieve the message and confirm scheduled_at |
DELETE | /v2/email_messages/{id}/schedule | Cancel the scheduled email (cleanup) |
One implementation note before the code: the sample uses the Telnyx Python SDK for the create and retrieve calls, but the reschedule call goes out as a raw HTTP PATCH — the SDK doesn't expose a patch-schedule method yet. To keep the flow legible regardless of which client you use, the snippets below show the wire format directly with requests.
Step 1: Schedule the email
import os
from datetime import datetime, timedelta, timezone
import requests
API = "https://api.telnyx.com/v2"
HEADERS = {
"Authorization": f"Bearer {os.environ['TELNYX_API_KEY']}",
"Content-Type": "application/json",
}
def iso(dt: datetime) -> str:
return dt.isoformat().replace("+00:00", "Z")
deliver_at = datetime.now(timezone.utc) + timedelta(hours=2)
resp = requests.post(
f"{API}/email_messages",
headers=HEADERS,
json={
"from": os.environ["TELNYX_EMAIL_FROM"],
"to": os.environ["TELNYX_EMAIL_TO"],
"subject": "Appointment reminder",
"text_body": "Your appointment is coming up. Reply to reschedule.",
"scheduled_at": iso(deliver_at),
},
)
message_id = resp.json()["data"]["id"]
Note the response code: 202 Accepted, not 201 Created. The message is accepted for scheduled delivery — it exists, it has an ID, and it will go out at scheduled_at.
Step 2: Reschedule it
This is the step the sample is named for, and it's one call:
new_time = datetime.now(timezone.utc) + timedelta(hours=4)
resp = requests.patch(
f"{API}/email_messages/{message_id}/schedule",
headers=HEADERS,
json={"scheduled_at": iso(new_time)},
)
No cancel. No recreate. Same message ID, same content, new delivery time. If you're tracking state against that message ID — delivery webhooks, per-recipient metadata, audit trails — it all stays intact.
Step 3: Try to move it into the past
Here's the guardrail:
past_time = datetime.now(timezone.utc) - timedelta(hours=1)
resp = requests.patch(
f"{API}/email_messages/{message_id}/schedule",
headers=HEADERS,
json={"scheduled_at": iso(past_time)},
)
assert resp.status_code == 422
The API treats a past scheduled_at as an invalid reschedule and returns 422 rather than quietly sending the email immediately. That distinction matters. "Send it now" is a policy decision; "refuse to send an email that was meant to go out hours ago" is a safety property. In a patient-communications workflow, the second one is what you want — a stale reminder sent late can be worse than no reminder at all.
The sample asserts the 422 explicitly, so the demo fails loudly if the guardrail ever disappears.
Step 4: Verify the new time
resp = requests.get(f"{API}/email_messages/{message_id}", headers=HEADERS)
print(resp.json()["data"]["scheduled_at"]) # matches the Step 2 timestamp
Trust, but verify: the reschedule response says it worked; the GET proves the stored scheduled_at actually moved.
Cleanup
requests.delete(f"{API}/email_messages/{message_id}/schedule", headers=HEADERS)
Cancels the scheduled email so the demo is repeatable and your outbox stays clean.
Setup
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/email-schedule-rescheduler
cp .env.example .env
pip install -r requirements.txt
python app.py
Fill in your .env:
| Variable | Purpose |
|---|---|
TELNYX_API_KEY | Your Telnyx API key (required in live mode) |
TELNYX_EMAIL_FROM | Verified sender address |
TELNYX_EMAIL_TO | Recipient address |
DEMO_MODE | true (default) logs requests without calling the API; false runs live |
If you don't have an API key yet, the Telnyx developer docs walk you through creating one. With DEMO_MODE=true you can run the full four-step flow immediately — no key required — and read the logged requests before going live.
Troubleshooting Quick Hits
TELNYX_API_KEY is required when DEMO_MODE=false— set the key in.envbefore going live.ERROR: reschedule failed with status 422— you tried to reschedule to a past or invalid timestamp. Use a future ISO 8601 UTC timestamp.FAIL: expected 422, got <status>— the invalid-reschedule step didn't get rejected. Double-check that the timestamp is genuinely in the past and the request body format is correct.
Wrap-Up
Rescheduling sounds like a CRUD detail until it's the difference between a patient making their follow-up appointment and missing it. The pattern here generalizes: treat schedule changes as updates to a single resource, verify state after every mutation, and prefer APIs that refuse invalid operations over ones that guess at intent.
Telnyx's Email API is part of the company's AI Communications Infrastructure — one programmable surface for building messaging workflows where timing is part of the contract. This sample shows what that looks like in practice: schedule, reschedule, reject the impossible, verify, clean up.