Bulk Shorten URLs from a CSV (via Python)
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
- Use a stable
Idempotency-Key
per row (alias or a UUID persisted in your sheet/DB). - For very large CSVs, chunk into batches of 500–1,000 rows.
- If you update OG images later, bump their querystring (
?v=2
) and re-share.