Read invoices by seq and catch a gap
Slflo issues the invoices — a van sale is invoiced at the shop, a delivered order when it’s
delivered — and the receipts and credit notes that go with them. Your ERP books them. Each of
the three streams is numbered by a gap-free counter per company, seq, given once when the
document is issued and never changed. Keep one integer per stream and you know exactly what is
new — and, because there are no holes, you can prove you hold every one.
| Stream | Read | seq is given when |
Cursor |
|---|---|---|---|
| Invoices | GET /invoices?filter[seq_after]=N |
the invoice is issued | GET /invoices/cursor |
| Payments | GET /payments?filter[seq_after]=N |
the receipt is issued | GET /payments/cursor |
| Returns | GET /returns?filter[seq_after]=N |
the return is credited (it has a credit_note_number) |
GET /returns/cursor |
Before you start: a token with invoices:read/write, payments:read/write and
returns:read/write — the sandbox token has them all. Unlike order acks, these acks work in any ERP mode.
The code is samples/python/invoices_by_seq.py and samples/csharp/InvoicesBySeq.cs, run
against a real Slflo in our build.
-
Choose where to start.
0reads the company’s whole history. To start from today, ask the cursor for the last number issued and store it:GET
/invoices/cursorTerminal window curl -X GET 'https://api.slflo.com/api/integration/v1/invoices/cursor' \-H 'Authorization: Bearer $SLFLO_TOKEN'Response
HTTP 200{"data": {"last_seq": 41,"high_water": 9140}} -
Read what was issued after your number, in issue order. Check each document’s
seqis the next one. Book it, remember the journal number, and move on. Stop at the first surprise.invoices_by_seq.py — read def read_new(api, erp, stream, last_seq):"""Everything issued after last_seq, in issue order; stops at the first gap."""entity, kind, amount = STREAMS[stream]read, gap = 0, Nonewhile True:page = api.get(f"/{stream}", {"filter[seq_after]": last_seq, "per_page": 100})documents = page["data"]for document in documents:if document["seq"] != last_seq + 1:# Can't happen on our side: the counter has no holes. If it does, your# bookkeeping skipped one: stop here, alert, and re-read from last_seq.gap = {"expected": last_seq + 1, "got": document["seq"]}breakjournal = fake_erp.post_journal(erp, kind, document["id"], document["version"], amount(document), document["seq"])document["journal"] = journallast_seq = document["seq"]read += 1booked = [d for d in documents if "journal" in d]if booked:ack(api, entity, booked)if gap or not page["meta"]["has_more"]:return read, last_seq, gapTerminal window curl -s "$SLFLO_BASE/invoices?filter%5Bseq_after%5D=41&per_page=100" \-H "Authorization: Bearer $SLFLO_TOKEN"# → {"data": [ …42, 43… ], "meta": {"per_page": 100, "last_seq": 43, "has_more": false}}InvoicesBySeq.cs // Invoices by the gap-free counter: keep one integer, read what was issued after it, book, ack.using System.Text.Json.Nodes;namespace Slflo.Samples;public sealed class SeqState{public long? LastSeq { get; set; }}public static class InvoicesBySeq{public static async Task<Dictionary<string, object?>> RunAsync(SlfloClient api){var state = StateFiles.Load<SeqState>("seq-cs.json");var erp = FakeErp.Open();if (state.LastSeq is null){// First run: start from now, not from the company's whole history.var cursor = await api.GetAsync("/invoices/cursor");state.LastSeq = cursor["data"]!["last_seq"]!.GetValue<long>();StateFiles.Save("seq-cs.json", state);}var read = 0;object? gap = null;while (gap is null){var page = await api.GetAsync("/invoices", new Dictionary<string, object> { ["filter[seq_after]"] = state.LastSeq, ["per_page"] = 100 });var items = new JsonArray();foreach (var invoice in page["data"]!.AsArray()){var seq = invoice!["seq"]!.GetValue<long>();if (seq != state.LastSeq + 1) { gap = new { expected = state.LastSeq + 1, got = seq }; break; } // alertvar id = invoice["id"]!.GetValue<string>();var journal = erp.PostJournal("invoice", id, invoice["version"]!.GetValue<long>());items.Add(new JsonObject { ["id"] = id, ["outcome"] = "ACCEPTED", ["external_ref"] = journal, ["external_number"] = journal });state.LastSeq = seq;read++;}if (items.Count > 0){var first = items[0]!["id"]!.GetValue<string>();await api.PostAsync("/acks", new JsonObject { ["entity"] = "invoice", ["items"] = items }, $"invoice-acks:{first}:{state.LastSeq}");}StateFiles.Save("seq-cs.json", state);if (!page["meta"]!["has_more"]!.GetValue<bool>()) break;}return new() { ["read"] = read, ["last_seq"] = state.LastSeq, ["gap"] = gap };}} -
Ack what you booked, many at once.
POST /ackstakes up to 500 of one kind; each item gets its own result, and one bad item never fails the rest.POST
/acksTerminal window curl -X POST 'https://api.slflo.com/api/integration/v1/acks' \-H 'Authorization: Bearer $SLFLO_TOKEN' \-H 'Idempotency-Key: invoice-acks:5f1c0e…' \-H 'Content-Type: application/json' \-d '{"entity":"invoice","items":[{"id":"inv_01J9","outcome":"ACCEPTED","external_ref":"JRN-000881","external_number":"JRN-000881"}]}'Response
HTTP 200{"data": [{"id": "inv_01J9","result": "applied","status": "ISSUED","version": 9120,"error": null}],"meta": {"applied": 1,"unchanged": 0,"failed": 0}} -
Pick up what changed after its ack.
seqshows each document once, at issue. A payment reversed, a cheque bounced, a return approved later — those change the document’sversionand make it pending again. Ask forfilter[ack]=pending, book the change (a reversing journal for a reversal), and ack it again at its new version with the same journal number.invoices_by_seq.py — changes since the ack def changed_since_ack(api, erp, stream):"""A document that changes after its ack (a payment reversed, a cheque bounced, a returnapproved) is pending again. seq never sees it twice; filter[ack]=pending does."""entity, kind, amount = STREAMS[stream]changed = []for page in api.pages(f"/{stream}", {"filter[ack]": "pending"}):for document in page["data"]:journal = fake_erp.journal_of(erp, kind, document["id"])if journal is None:continue # never booked: it arrives through its seq (a return once it is credited)if document.get("reversal"):fake_erp.post_journal(erp, f"{kind}-reversal", document["id"], document["version"], amount(document))document["journal"] = journal # the answer stays the journal that booked itchanged.append(document)if changed:ack(api, entity, changed)return len(changed)
Run it
Section titled “Run it”export SLFLO_TOKEN='sfi_live_…' SLFLO_BASE='https://api.slflo.com/api/integration/v1'python3 invoices_by_seq.pyOn the hosted sandbox the first run reads the day’s two invoices, two receipts and one credit note:
{"changed": {"invoices": 0, "payments": 0, "returns": 0}, "invoices": {"gap": null, "last_seq": 2, "read": 2}, "payments": {"gap": null, "last_seq": 2, "read": 2}, "returns": {"gap": null, "last_seq": 1, "read": 1}}A second run reads nothing new. After the office reverses a payment (on the sandbox, ask us),
the next run reads no new receipt but reports "changed": {"payments": 1} — the reversal,
booked as a reversing journal.
Detecting a gap
Section titled “Detecting a gap”There are two kinds, and they mean different things.
-
In the answer: you asked after 41 and the first document is 43. The counter has no holes, so this can’t come from Slflo; it means your stored number is wrong (two jobs sharing it, a restored settings table). The sample stops the stream, reports
"gap": {"expected": 42, "got": 43}, exits non-zero and saves nothing past 41. Alert someone; don’t skip ahead. -
In your books: the numbers you read are all there, but a journal went missing on your side (deleted by hand, a partial restore). Because
seqhas no holes, you can list every number from 1 to your last one and look for the missing. To fetch number n again, ask forfilter[seq_after]=n-1&per_page=1:invoices_by_seq.py — check def check(api, erp, state):"""Holes in the ERP's own books (a journal deleted by hand, a restore): re-read and re-book them."""found = {}for stream, (entity, kind, amount) in STREAMS.items():booked = set(fake_erp.booked_seqs(erp, kind))missing = [n for n in range(1, state.get(stream, 0) + 1) if n not in booked]for seq in missing:page = api.get(f"/{stream}", {"filter[seq_after]": seq - 1, "per_page": 1})document = page["data"][0]fake_erp.post_journal(erp, kind, document["id"], document["version"], amount(document), document["seq"])found[stream] = missingreturn foundTerminal window python3 invoices_by_seq.py check # {"missing": {"invoices": [1], "payments": [], "returns": []}}
When it goes wrong
Section titled “When it goes wrong”| What you see | Why | What to do |
|---|---|---|
A return you know about doesn’t arrive by seq |
It hasn’t been credited yet (waiting for approval) | It gets a seq when credited; until then it’s visible by version or as pending |
409 ALREADY_ACKNOWLEDGED on an item |
You answered this version before with a different journal number | Keep one journal number per document; re-ack with the same one |
409 EXTERNAL_REF_CONFLICT on an item |
That journal number is already on another document | One journal per document |
400 INVALID_REQUEST naming seq_after |
filter[seq_after] sent with a version cursor |
Send one cursor, not both |
| The same document read twice | Your number wasn’t saved after the last page | Harmless if your journal posting is keyed by (document id, version), as the sample’s is |

