Email API

Reschedule, Don't Recreate: Building an Email Schedule Rescheduler with Telnyx

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:

  1. Schedule an email with a future scheduled_at timestamp (POST /v2/email_messages → 202 Accepted with a message ID).
  2. Reschedule it to a new future time (PATCH /v2/email_messages/{id}/schedule).
  3. Attempt an invalid reschedule to a timestamp in the past — and confirm the API rejects it with 422.
  4. 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:

MethodEndpointPurpose
POST/v2/email_messagesCreate a scheduled email with a future scheduled_at
PATCH/v2/email_messages/{id}/scheduleReschedule to a new future time
GET/v2/email_messages/{id}Retrieve the message and confirm scheduled_at
DELETE/v2/email_messages/{id}/scheduleCancel 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:

VariablePurpose
TELNYX_API_KEYYour Telnyx API key (required in live mode)
TELNYX_EMAIL_FROMVerified sender address
TELNYX_EMAIL_TORecipient address
DEMO_MODEtrue (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 .env before 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.

Frequently Asked Questions

Can a scheduled email be moved without creating a new message?

Yes. Send a PATCH request to /v2/email_messages/{id}/schedule with a new future scheduled_at value. The original message ID, content, recipients, tags, and metadata remain unchanged.

What happens when the new delivery time is in the past?

The Email API rejects a missing, invalid, or non-future scheduled_at value with a 422 response. It does not silently turn the request into an immediate send.

How can an application confirm that rescheduling worked?

The PATCH response includes the updated message representation. The application can also retrieve the message with GET /v2/email_messages/{id} and verify its stored scheduled_at value.

Can the scheduled email still be cancelled?

Yes. Send DELETE /v2/email_messages/{id}/schedule before delivery. A successful cancellation returns the message in the cancelled state.

Ready to build with low-latency voice AI?

Join developers building the future of real-time conversations