تخطَّ إلى المحتوى

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.

  1. 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'
  2. Check who you are. GET /me needs 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"
    ]
    }
    }
  3. Pull orders to the end. Start from version 0 and follow meta.next_version_after until meta.has_more is false. 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"

    -g stops curl reading the brackets in filter[…] as a pattern. One order, shortened (the sandbox’s two orders, SO-000001 and SO-000002, are already DELIVERED; 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" }
    }
  4. Ack the order once your ERP has created its sales order. The Idempotency-Key makes 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"
    }
    }
    }
  5. Push a customer by your key. Send it, then send the same body again under a new Idempotency-Key: the first answers created, the second unchanged — and nothing is downloaded to the phones again. (Under the same key the second call would replay the first answer, created, with Idempotent-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}'

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.

Terminal window
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"}.

quickstart.py
"""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 sys
import 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 = [], 0
while 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 unchanged
print("CUST-0042:", " then ".join(results), file=sys.stderr)
summary["customer"] = results
report(summary)
slflo.py, the client it uses
slflo.py
"""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 json
import os
import sys
import time
import urllib.error
import urllib.parse
import 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)