Bulk Shorten URLs from a CSV (via Python)

~10 min

1) Prepare your CSV

Create input.csv with headers (unused columns can be left blank):

url,title,description,image,alias,qr_enabled,expires_at
https://example.com/product/1,Widget 1,Best widget.,https://cdn.example.com/og/w1.jpg,widget-1,true,
https://example.com/product/2,Widget 2,Now shipping.,https://cdn.example.com/og/w2.jpg,widget-2,false,2025-12-31T23:59:59Z

2) Python script

Save as bulk_shorten.py and run in a virtualenv. Adjust endpoint/fields to your API.

#!/usr/bin/env python3
import csv, time, os, sys, uuid, json
import requests

API_URL = os.getenv("OGLI_API_URL", "https://api.ogli.sh/link")
API_KEY = os.getenv("OGLI_API_KEY")  # set this in your environment
INPUT   = os.getenv("INPUT_CSV", "input.csv")
OUTPUT  = os.getenv("OUTPUT_CSV", "output.csv")
SLEEP_S = float(os.getenv("REQUEST_SLEEP_S", "0.2"))  # throttle between calls

if not API_KEY:
    print("Missing OGLI_API_KEY env var", file=sys.stderr); sys.exit(1)

def create_link(row):
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        # optional idempotency key so retries don't duplicate:
        "Idempotency-Key": row.get("alias") or str(uuid.uuid4())
    }
    body = {
        "url": row["url"],
        "alias": row.get("alias") or None,
        "og": {
            "title": row.get("title") or None,
            "description": row.get("description") or None,
            "image": row.get("image") or None
        },
        "qr": { "enabled": (row.get("qr_enabled","").lower() == "true") }
    }
    if row.get("expires_at"):
        body["expiresAt"] = row["expires_at"]

    # remove None values
    def prune(obj):
        if isinstance(obj, dict):
            return {k: prune(v) for k, v in obj.items() if v not in (None,"")}
        return obj
    body = prune(body)

    tries, backoff = 0, 0.5
    while True:
        tries += 1
        try:
            resp = requests.post(API_URL, data=json.dumps(body), headers=headers, timeout=15)
            if resp.status_code in (200,201):
                data = resp.json()
                return {"status": resp.status_code, "url": data.get("url"), "id": data.get("id"), "error": ""}
            elif resp.status_code in (429, 500, 502, 503, 504):
                # retry with backoff
                time.sleep(backoff); backoff = min(backoff*2, 8)
                continue
            else:
                return {"status": resp.status_code, "url": "", "id": "", "error": resp.text[:500]}
        except Exception as e:
            if tries <= 4:
                time.sleep(backoff); backoff = min(backoff*2, 8)
                continue
            return {"status": 0, "url": "", "id": "", "error": str(e)}

def main():
    with open(INPUT, newline="", encoding="utf-8") as fin, open(OUTPUT, "w", newline="", encoding="utf-8") as fout:
        reader = csv.DictReader(fin)
        fieldnames = reader.fieldnames + ["status","url","id","error"]
        writer = csv.DictWriter(fout, fieldnames=fieldnames)
        writer.writeheader()
        for row in reader:
            if not row.get("url"):
                row.update({"status":0,"url":"","id":"","error":"missing url"})
                writer.writerow(row); continue
            result = create_link(row)
            row.update(result)
            writer.writerow(row)
            time.sleep(SLEEP_S)

if __name__ == "__main__":
    main()

3) Run it

python3 -m venv .venv && source .venv/bin/activate
pip install requests
export OGLI_API_KEY="<your key>"
python bulk_shorten.py
# See output.csv for results

Tips


← Back to all how-tos