Skip to content

About

Public API for Soumit — Moroccan public procurement intelligence. Tenders, award history, AI analyses and Go/No-Go verdicts. OpenAPI 3.1 spec, Python & TypeScript clients.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Soumit Public API

Programmatic access to Moroccan public procurement — tenders, award history, AI analyses and Go/No-Go verdicts.

OpenAPI 3.1 API status License

This repository is the developer surface of Soumit: the API reference, the machine-readable contract, official clients for Python and TypeScript, and runnable examples.

The API is live. You can see its shape right now, without a key:

curl https://soumit.ma/api/v1
{
  "object": "service_index",
  "version": "1.0.0",
  "openapi_url": "/api/v1/openapi.json",
  "resources": ["/api/v1/tenders", "/api/v1/awards", "…"],
  "scopes": ["tenders:read", "awards:read", "…"],
  "rate_limits": { "read_per_hour": 1000, "write_per_hour": 300 }
}

The problem this solves

Moroccan public buyers publish thousands of consultations a year on marchespublics.gov.ma. Each one arrives as a dossier de consultation des entreprises (DCE): a ZIP of scanned PDFs, a CPS running to a hundred pages, an administrative règlement, price schedules, sometimes a plan set.

For a large firm with a bid desk, that is a staffing line. For the SME that makes up most of the supplier base, it is the reason they do not bid. The decision that matters — is this one worth the three days it will cost to answer? — has to be made from documents that take a day to read.

Soumit ingests the notice and its package, extracts the facts that decide the question, and answers it with a Go / No-Go verdict whose every factor is traceable to a source. This API exposes that as data you can build on.

What you get

Resource What it is Shared or private
Tenders The published corpus: buyer, object, budget, procedure, region, deadline Shared — same rows for everyone
Awards Attribution records (procès-verbaux): who won, for how much, against what estimate Shared
Analyses Structured extraction of a consultation, with a quality tier and per-field provenance Private to your organisation
Verdicts Go / Watch / No-Go, factor by factor, each with status, weight and provenance Private
Catalogue What your organisation can supply — references, cost basis, registrations, lead times Private, read + write
Pursuits Your bid pipeline: stage, decision, submission, outcome Private, read + write
Events & webhooks Signed, retried delivery of the four event types, plus a pollable log Private

Corpus as of 2026-08-14 — 3,693 tenders spanning 2025-11-24 to 2026-08-13, 1,838 award records, 1,998 analyses. Ingestion runs daily.

Quickstart

pip install ./clients/python        # or: npm install ./clients/typescript
from soumit import Soumit

soumit = Soumit(api_key="sk_live_…")

# Everything closing in the next 30 days, paginated automatically.
for tender in soumit.tenders.auto_paginate(
    deadline_before="2026-09-13", status="open"
):
    print(tender["deadline"], tender["buyer"]["name"], tender["title"])
import { Soumit } from "@soumit/api";

const soumit = new Soumit({ apiKey: process.env.SOUMIT_API_KEY! });

for await (const award of soumit.awards.autoPaginate({ awardedAfter: "2026-01-01" })) {
  console.log(award.awardee.name, award.awarded_amount, award.currency);
}

Or with no dependencies at all:

curl -H "Authorization: Bearer $SOUMIT_API_KEY" \
     "https://soumit.ma/api/v1/tenders?limit=5&status=open"

More in examples/ — market-share analysis from award history, a deadline watcher, and a signature-verifying webhook receiver.

Getting a key

Keys are issued on request while the API is in its pilot phase. Email abdel@envoisms.ma saying what you want to build and which scopes you need, and you will get one.

A key belongs to exactly one organisation, carries only the scopes you asked for, and is shown once. Read and write are separate scopes for every resource that has both, so a reporting integration that must never mutate anything is expressible without trust.

Documentation

Authentication & scopes Bearer keys, the scope vocabulary, wildcards, revocation
Pagination Opaque cursors, and why not offsets
Errors The complete code table and what to retry
Rate limits Budgets, headers, backoff
Webhooks Signature verification, replay defence, retry schedule
Data model Every resource field by field, and the honesty contract
Architecture How the platform is built, and the decisions behind it
openapi.json The full OpenAPI 3.1 contract

Three design decisions worth reading

Most of this API is conventional on purpose — Bearer tokens, cursor pagination, Idempotency-Key, HMAC-signed webhooks. Convention is a feature: nothing here should need explaining to someone who has integrated a payments API. Three things are deliberate departures.

1. A field it cannot establish is null. Always.

The product exists so somebody can decide whether to commit three days of engineering time to a bid. A fabricated budget is worse than a blank one, because a blank one gets checked.

So nothing is ever filled with a plausible substitute. A tender whose notice published no budget has budget.amount: null — never a sector average. A Go/No-Go factor with no input has status: "unknown" and value: null, and its weight leaves the denominator, so it neither rewards nor punishes the score. When the engine has no blocker and no defensible score, it publishes total_score: null with a score_withheld_reason rather than a precise-looking number built on nothing.

Every response carries an unavailable map giving the stable reason code for each missing field, so a client can distinguish not published, published but unparseable, and not yet extracted — three situations that mean different things to a bidder.

2. Quality is part of the payload, not an assumption.

Every analysis carries a quality.tier:

  • decision_ready — passed every readiness gate
  • review_required — did not; a human should look
  • degraded — produced from impaired source material (a scan that would not OCR)

...plus per-field provenance: exact (read verbatim from the consultation), derived (computed from something that was), inferred, or missing (looked for, not found). Verdict factors record whether each input was grounded — from a record — or asserted by a model.

An LLM in the pipeline will occasionally be confidently wrong. Rather than pretend otherwise, the API reports how much of each answer rests on something checkable, and lets the consumer set its own bar.

3. The spec is generated by the thing it describes.

GET /api/v1/openapi.json is built per request from the same operation registry the handlers read their required scopes from. It physically cannot describe a different version of the API than the deployment serving it, because there is only one table. A checked-in spec is a second source of truth that drifts the first time someone adds a query parameter.

The copy vendored in this repository is therefore a snapshot, and CI re-fetches the live document daily and fails on drift rather than letting the docs quietly go stale.


How the platform is built

marchespublics.gov.ma
        │
        ▼
  ┌───────────┐   notice + DCE package (ZIP → PDF → OCR)
  │  Ingest   │   Python, scheduled
  └─────┬─────┘
        ▼
  ┌───────────┐   structured extraction: object, lots, budget, deadlines,
  │  Extract  │   required documents, qualifications, penalties
  └─────┬─────┘   each field tagged exact / derived / inferred / missing
        ▼
  ┌───────────┐   factor scoring against the organisation's catalogue
  │  Verdict  │   and recorded outcomes → GO / WATCH / NO_GO
  └─────┬─────┘
        ▼
  ┌───────────┐   Cloudflare D1 (system of record) · R2 (documents)
  │   Edge    │   Next.js 15 on Pages · /api/v1 on Workers
  └─────┬─────┘
        ▼
   this API  ──▶  signed webhooks ──▶  your systems
Layer Choice
Edge compute & API Cloudflare Workers, Next.js 15 (App Router) on Pages
System of record Cloudflare D1 (serverless SQLite), 18 forward migrations
Document storage Cloudflare R2 — DCE archives, generated artefacts
Heavy extraction Modal — OCR and long-running document jobs off the edge
Language models Provider-neutral OpenAI-compatible client; models are configuration
Pipeline Python 3.11, ~1,400 tests
Frontend React 19, TypeScript, Tailwind

Tenancy fails closed. A database binding with no tenant scope throws. There is no fail-open fallback and no override flag: a query against a private table that forgets organization_id raises rather than returning another organisation's rows. The handful of paths that legitimately have no session must declare it at the call site with a written reason, and that declaration is itself refused once a second organisation exists — so it cannot rot into a leak. The shared corpus (tenders, awards) is explicitly separated from the private overlay, which is why the same endpoint can serve public data and private analysis without either leaking into the other.

Idempotency, at-least-once, and an audit trail. Writes accept Idempotency-Key: the same key with the same body replays the first response, the same key with a different body is a 409. Webhook delivery is at-least-once with six retries over 24 hours, every attempt recorded and readable from /api/v1/webhook-deliveries — the honest answer to "did you actually send it?" is a row, not a shrug.


Repository layout

.
├── openapi.json              # OpenAPI 3.1 contract (snapshot; CI checks drift)
├── docs/                     # Guides — auth, pagination, errors, webhooks, data model
├── clients/
│   ├── python/               # pip-installable client, no dependencies beyond httpx
│   └── typescript/           # zero-dependency client, fetch-based
├── examples/
│   ├── python/               # deadline watcher, award market-share analysis
│   └── node/                 # signature-verifying webhook receiver
└── .github/workflows/        # daily spec-drift check

Status & versioning

v1 is stable and in pilot use. Additive changes — new fields, new optional parameters, new event types — ship without a version bump, so clients must ignore unknown fields. Anything that would break a conforming client gets /api/v2 and an announced deprecation window on v1.

Data provenance & licence

The tender and award corpus is derived from notices published by Moroccan public buyers on the national procurement portal — public information by law. Soumit adds normalisation, matching and extraction on top. Document excerpts from third -party consultation packages are not republished through this API; analyses report only whether an excerpt supports a field, never the excerpt itself.

The code in this repository — clients, examples, docs — is MIT licensed. Access to the hosted API is governed by its own terms.


Built and maintained by @n4ouri.

Key requests and questions: abdel@envoisms.ma Bugs in the clients or docs: open an issue.

About

Public API for Soumit — Moroccan public procurement intelligence. Tenders, award history, AI analyses and Go/No-Go verdicts. OpenAPI 3.1 spec, Python & TypeScript clients.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages