Bird

Reply to incoming SMS from your application

Answer HOURS with the sample studio's opening times. Your application owns that response. Bird supplies the inbound event and the outbound send operation.

1. Receive a real inbound message

Start with the delivery-receipt receiver. Subscribe it to sms.received and use an owned number that supports inbound SMS in its destination. Alphanumeric sender IDs cannot receive a handset reply.
From a phone you control, send HOURS to that number. Confirm that the receiver saved sms.received. Inbound data.to is your number; data.from is the subscriber. They exchange roles in the reply. See the event contract.

2. Process a bounded reply job

Set BIRD_SMS_FROM to that owned number and BIRD_API_KEY to a key with SMS sending permission. Save this as reply_worker.py beside receiver.py, then run python reply_worker.py once.
The example processes stored HOURS events. Its durable replies table claims an inbound message once, so an event replay or a second worker invocation does not automatically send another reply.
Codevoorbeeld
import json
import logging
import os
from uuid import NAMESPACE_URL, uuid5

from bird import Bird, APIError
from receiver import database

# Sample business content belongs to this application.
REPLY = "Our demo studio is open Monday to Friday, 09:00 to 17:00."
owned_number = os.environ["BIRD_SMS_FROM"]
db = database()
db.execute("""CREATE TABLE IF NOT EXISTS replies (
    inbound_id TEXT PRIMARY KEY, request_key TEXT, recipient TEXT, body TEXT,
    state TEXT, message_id TEXT)""")
db.commit()
try:
    for (raw,) in db.execute("SELECT body FROM events").fetchall():
        event = json.loads(raw)
        if event["type"] != "sms.received":
            continue
        data = event["data"]
        if data["to"] != owned_number or data.get("text", "").strip().upper() != "HOURS":
            continue
        inbound_id = data["workspace_id"] + ":" + data["sms_id"]
        request_key = str(uuid5(NAMESPACE_URL, "bird-sms-hours:" + inbound_id))
        with db:
            added = db.execute("INSERT OR IGNORE INTO replies VALUES (?, ?, ?, ?, 'pending', NULL)",
                               (inbound_id, request_key, data["from"], REPLY)).rowcount
        if not added:
            continue
        with db:
            db.execute("UPDATE replies SET state='sending' WHERE inbound_id=?", (inbound_id,))
        try:
            with Bird(api_key=os.environ["BIRD_API_KEY"]) as bird:
                message = bird.sms.send(
                    from_=owned_number, to=data["from"], text=REPLY,
                    category="service", options={"idempotency_key": request_key},
                )
        except APIError:
            logging.exception("SMS reply failed; reconcile the saved attempt")
            with db:
                db.execute("UPDATE replies SET state='review' WHERE inbound_id=?", (inbound_id,))
            continue
        with db:
            db.execute("UPDATE replies SET state='accepted', message_id=? WHERE inbound_id=?",
                       (message.id, inbound_id))
finally:
    db.close()
Only HOURS triggers this sample. Other messages remain stored for another application handler. In particular, the worker never replies to STOP; Bird's configured keyword and opt-out handling remains responsible for its supported keywords. This is not a complete multilingual consent handler.

3. Inspect and recover

Read the replies table and the outbound message's delivery receipt. accepted in this table means the API accepted the reply, not that the phone received it.
Worker recordWhat to do
accepted with a message IDRead that message or consume its delivery events.
reviewInspect the error and original send in Bird before retrying.
sending after a worker crashReconcile the uncertain attempt; do not treat it as unsent.
pending after interruptionInspect whether sending began before resuming the saved operation.
The worker deliberately leaves interrupted or failed attempts visible. A production worker needs an operator or scheduled reconciliation path that retains the original recipient, content and request key. Follow idempotency retention and recovery; a new key or a retry beyond retained responses can create another send.
For high volume, claim bounded batches of unprocessed events in a shared database instead of scanning the local demonstration table. Keep the business action, deduplication record and pending reply together. Apply your retention and erasure policy to the event and reply records.

Continue the integration