Quickstart
هذا المحتوى غير متوفر بلغتك بعد.
This walks the whole round trip against the sandbox: a demo company with customers, products and orders already in it. It takes about ten minutes.
-
Get a sandbox token. Ask for sandbox access; you receive a token for the sandbox company. It’s an ordinary token of that company and starts with
sfi_live_, like every token the console issues. Keep it out of source control and put it in an environment variable:Terminal window export SLFLO_TOKEN='sfi_live_…'export SLFLO_BASE='https://api.slflo.com/api/integration/v1' -
Check who you are.
GET /meneeds no scope. It names your client, the company the token belongs to, your scopes, when the token expires and your rate limit.Terminal window curl -s "$SLFLO_BASE/me" -H "Authorization: Bearer $SLFLO_TOKEN"On the sandbox the answer looks like this (ids, dates and the prefix are your token’s; the client holds every scope, shortened here):
GET
/meنافذة طرفيّة curl -X GET 'https://api.slflo.com/api/integration/v1/me' \-H 'Authorization: Bearer $SLFLO_TOKEN'Response
HTTP 200{"data": {"client": {"id": "ic_01J9","name": "Sandbox","system": "OTHER","token_prefix": "sfi_live_AB12CD34","token_expires_at": "2027-10-07T00:00:00Z","rate_limit_per_minute": 120},"company": {"id": "t-sandbox","code": "t-sandbox","name": "Nile Fresh (sandbox)"},"scopes": ["orders:read","orders:write","invoices:read","invoices:write","payments:read","payments:write","returns:read","returns:write","customers:write","catalog:write","prices:write","balances:write"]}} -
Pull orders to the end. Start from version
0and followmeta.next_version_afteruntilmeta.has_moreisfalse. Each order arrives with its customer, branch, rep, lines, VAT and totals — and every reference carries both Slflo’s id and your key.Terminal window curl -sg "$SLFLO_BASE/orders?filter[version_after]=0&per_page=100" \-H "Authorization: Bearer $SLFLO_TOKEN"quickstart.py — pull orders, after = [], 0while True:page = api.get("/orders", {"filter[version_after]": after, "per_page": 100})orders += page["data"]if not page["meta"]["has_more"]:breakafter = page["meta"]["next_version_after"]print(len(orders), "orders; watermark", orders[-1]["version"] if orders else after, file=sys.stderr)summary["orders"] = len(orders)-gstops curl reading the brackets infilter[…]as a pattern. One order, shortened (the sandbox’s two orders, SO-000001 and SO-000002, are alreadyDELIVERED; this one waits for your ERP):{"id": "ord_01J9X", "number": "SO-000123", "version": 18342, "status": "ERP_PENDING","order_date": "2026-10-05", "currency": "EGP", "idempotency_key": "op_7f3a","customer": { "id": "C0000001", "code": "CUST-1", "external_ref": "CUST-0042" },"lines": [{ "line_number": 1, "product": { "code": "SKU-COLA", "external_ref": "ITEM-1001" },"unit": { "code": "CASE" }, "quantity": "10.000", "unit_price": "125.50","tax": [{ "code": "VAT", "rate": "14.00", "amount": "175.70" }], "line_total": "1430.70" }],"totals": { "net": "1255.00", "tax": "175.70", "gross": "1430.70" }} -
Ack the order once your ERP has created its sales order. The
Idempotency-Keymakes the call safe to retry; use something you already have, like your sales order number.POST
/orders/ord_01J9X/ackنافذة طرفيّة curl -X POST 'https://api.slflo.com/api/integration/v1/orders/ord_01J9X/ack' \-H 'Authorization: Bearer $SLFLO_TOKEN' \-H 'Idempotency-Key: SO-0012345' \-H 'Content-Type: application/json' \-d '{"outcome":"ACCEPTED","external_ref":"SO-0012345","external_number":"SO-0012345"}'Response
HTTP 200{"data": {"id": "ord_01J9X","number": "SO-000123","status": "ERP_CONFIRMED","version": 18343,"result": "applied","ack": {"outcome": "ACCEPTED","external_ref": "SO-0012345","external_number": "SO-0012345","acked_version": 18343,"acked_at": "2026-10-05T10:42:00Z"}}} -
Push a customer by your key. Send it, then send the same body again under a new
Idempotency-Key: the first answerscreated, the secondunchanged— and nothing is downloaded to the phones again. (Under the same key the second call would replay the first answer,created, withIdempotent-Replayed: true— see retrying safely.)PUT
/customers/CUST-0042نافذة طرفيّة curl -X PUT 'https://api.slflo.com/api/integration/v1/customers/CUST-0042' \-H 'Authorization: Bearer $SLFLO_TOKEN' \-H 'Idempotency-Key: CUST-0042@2026-10-05T10:40:00Z' \-H 'Content-Type: application/json' \-d '{"name":{"en":"Al Amal Market","ar":"سوق الأمل"},"address":"12 Tahrir St, Giza","credit_limit":50000,"blocked":false,"source_version":"2026-10-05T10:40:00Z"}'Response
HTTP 200{"data": {"entity": "customer","id": "C0000001","external_ref": "CUST-0042","result": "created"}}Terminal window curl -s -X PUT "$SLFLO_BASE/customers/CUST-0042" \-H "Authorization: Bearer $SLFLO_TOKEN" -H "Content-Type: application/json" \-H "Idempotency-Key: CUST-0042@$(date +%s)" \-d '{"name":{"en":"Al Amal Market","ar":"سوق الأمل"},"address":"12 Tahrir St, Giza","credit_limit":50000,"blocked":false}'
The whole quickstart as one script
Section titled “The whole quickstart as one script”Steps 2 to 5 in Python, with only the standard library. Our build runs this file against a real
Slflo on every change. It needs the small client slflo.py beside it, which handles 401, 429
and retries — see Retries, 429 and token rotation.
export SLFLO_TOKEN='sfi_live_…'python3 quickstart.py# {"client": "Sandbox", "customer": ["created", "unchanged"], "orders": 2}On the hosted sandbox it pulls the day’s two orders and acks none, because neither waits for
your ERP. When an order does wait (status ERP_PENDING), the script acks the first one and the
line also holds "acked": {"id": …, "result": "applied", "status": "ERP_CONFIRMED"}.
"""The quickstart as one script: check the token, pull orders to the end, ack one, push a customer.
SLFLO_TOKEN=sfi_live_... python3 quickstart.py"""import sysimport uuid
from slflo import ApiError, Slflo, report
api = Slflo()summary = {}
me = api.get("/me")["data"]print(me["client"]["name"], "·", me["company"]["name"], "·", ", ".join(me["scopes"]), file=sys.stderr)summary["client"] = me["client"]["name"]
orders, after = [], 0while True: page = api.get("/orders", {"filter[version_after]": after, "per_page": 100}) orders += page["data"] if not page["meta"]["has_more"]: break after = page["meta"]["next_version_after"]print(len(orders), "orders; watermark", orders[-1]["version"] if orders else after, file=sys.stderr)summary["orders"] = len(orders)
waiting = [o for o in orders if o["status"] == "ERP_PENDING"]if waiting: order = waiting[0] your_number = "SO-" + order["number"].split("-")[-1] # your ERP's sales order number answer = api.post(f"/orders/{order['id']}/ack", {"outcome": "ACCEPTED", "external_ref": your_number, "external_number": your_number}, key=your_number)["data"] print(order["number"], "→", answer["status"], answer["result"], file=sys.stderr) summary["acked"] = {"id": order["id"], "status": answer["status"], "result": answer["result"]}
customer = {"name": {"en": "Al Amal Market", "ar": "سوق الأمل"}, "address": "12 Tahrir St, Giza", "credit_limit": 50000, "blocked": False, "source_version": "2026-10-05T10:40:00Z"}results = []for attempt in range(2): pushed = api.put("/customers/CUST-0042", customer, key=f"CUST-0042:{uuid.uuid4().hex}")["data"] results.append(pushed["result"]) # created, then unchangedprint("CUST-0042:", " then ".join(results), file=sys.stderr)summary["customer"] = results
report(summary)slflo.py, the client it uses
"""A small client for the Slflo integration API, with the rules every connector needs.
- 401: the token can't be used any more. Stop the job and alert; retrying won't help.- 429: wait the Retry-After seconds, then send the same call again.- 5xx, a lost connection, or 409 REQUEST_IN_PROGRESS: back off (1 s, 2 s, 4 s ...) and retry. Every write carries an Idempotency-Key, so a retry can never apply twice.- Any other 4xx: raise ApiError with the code, message and details; fix the data.
Standard library only (Python 3.8+). Settings come from the environment: SLFLO_TOKEN your token, sfi_live_... (sfi_test_... on a copy of Slflo you run yourself) SLFLO_BASE default https://api.slflo.com/api/integration/v1"""import jsonimport osimport sysimport timeimport urllib.errorimport urllib.parseimport urllib.request
DEFAULT_BASE = "https://api.slflo.com/api/integration/v1"
class TokenRefused(Exception): """401: TOKEN_INVALID, TOKEN_EXPIRED, TOKEN_REVOKED or COMPANY_SUSPENDED."""
def __init__(self, code, message): super().__init__(f"{code}: {message}") self.code = code
class ApiError(Exception): """A refusal to fix on your side (or a call that kept failing)."""
def __init__(self, status, error): self.status = status self.code = error.get("code", "") self.details = error.get("details", []) self.retryable = error.get("retryable", False) self.request_id = error.get("request_id") super().__init__(f"{status} {self.code}: {error.get('message', '')}")
class Slflo: def __init__(self, token=None, base=None, max_attempts=6, first_backoff=1.0, timeout=30): self.token = token or os.environ["SLFLO_TOKEN"] self.base = (base or os.environ.get("SLFLO_BASE") or DEFAULT_BASE).rstrip("/") self.max_attempts = max_attempts self.first_backoff = first_backoff self.timeout = timeout
def get(self, path, query=None): return self.call("GET", path, query=query)
def put(self, path, body, key, query=None): return self.call("PUT", path, query=query, body=body, key=key)
def post(self, path, body, key): return self.call("POST", path, body=body, key=key)
def call(self, method, path, query=None, body=None, key=None): url = self.base + path if query: url += "?" + urllib.parse.urlencode(query) headers = {"Authorization": f"Bearer {self.token}", "Accept": "application/json"} data = None if body is not None: data = json.dumps(body).encode() headers["Content-Type"] = "application/json" if key is not None: headers["Idempotency-Key"] = key
backoff = self.first_backoff for attempt in range(1, self.max_attempts + 1): last = attempt == self.max_attempts request = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(request, timeout=self.timeout) as response: return json.loads(response.read() or b"{}") except urllib.error.HTTPError as refused: status, error = refused.code, _error_of(refused) retry_after = refused.headers.get("Retry-After") if status == 401: raise TokenRefused(error.get("code", "TOKEN_INVALID"), error.get("message", "")) if status == 429: wait = int(retry_after) if retry_after and retry_after.isdigit() else backoff _log(f"{method} {path}: 429 {error.get('code', '')}, waiting {wait}s (Retry-After)") if last: raise ApiError(status, error) time.sleep(wait) continue if status >= 500 or error.get("code") == "REQUEST_IN_PROGRESS": if last: raise ApiError(status, error) wait = int(retry_after) if retry_after and retry_after.isdigit() else backoff _log(f"{method} {path}: {status} {error.get('code', '')}, backing off {wait}s") time.sleep(wait) backoff *= 2 continue raise ApiError(status, error) except OSError as lost: # connection refused or reset, timeout # The answer was lost: the call may or may not have landed. The retry # carries the same Idempotency-Key, so it can't apply twice. if last: raise _log(f"{method} {path}: {lost}, backing off {backoff}s") time.sleep(backoff) backoff *= 2 raise AssertionError("unreachable")
def pages(self, path, query=None, after=0, per_page=100): """Every page of a collection by the version cursor, oldest change first.""" while True: page = self.get(path, {**(query or {}), "filter[version_after]": after, "per_page": per_page}) yield page if not page["meta"]["has_more"]: return after = page["meta"]["next_version_after"]
def _error_of(refused): try: return json.loads(refused.read()).get("error", {}) except ValueError: return {} # a gateway's HTML or plain-text answer
def _log(message): print(f"slflo: {message}", file=sys.stderr, flush=True)
def report(summary): """Each sample ends by printing one line of JSON: what it did.""" print(json.dumps(summary, sort_keys=True), flush=True)Where next
Section titled “Where next”- Build a pull connector — orders into sales orders, once each.
- Pulling documents — staging, filters, and recovering after a lost cursor.
- The invoice counter —
seqfor invoices, receipts and credit notes. - Acks — accepting, rejecting, and what “send again” does.
- API reference — every operation.

