# Partner API contract Status: draft full contract; foundation read endpoints implemented 2026-11-12. This is the integration contract to implement after the official Modian document baseline and first supported invoice patterns are confirmed. It does not claim a working API. Base URL: `https://tax.didis.ir/api/v1/`. English field names and machine codes; Persian user-facing messages. [Research](research/modian.md) owns upstream facts; [architecture](architecture.md) owns queue and persistence rules. ## Principles and authentication The API accepts business documents, issues its own receipt, and handles signing, upstream submission and inquiry. A receipt means **durably accepted by nptak-tax**, never already registered in Modian. HTTP `202` expresses unfinished processing; the response links to a status resource ([RFC 9110 §15.3.3](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.3)). Partners receive separate opaque bearer API credentials for production and our isolated test environment. A simulated test adapter is not an official Modian sandbox; official test connectivity remains unverified. Send `Authorization: Bearer ` over HTTPS. Issue through the restricted management dashboard; show the secret once, retain a verifier, allow overlap during rotation and immediate revocation. Token scopes: `profiles:read`, `profiles:write`, `credentials:write`, `submissions:write`, `submissions:read`. A token is bound to a partner organization and explicit taxpayer grants; an object ID alone never grants access. Every lookup, uniqueness constraint and idempotency operation includes the organization boundary. Unauthorized object identifiers return `404` consistently. Human dashboard sessions use a separate authentication flow; SMS OTP is not the partner API credential. No passwords, private keys, authorization headers or full taxpayer payloads in access logs, exception reports, URLs or example fixtures. Responses expose safe credential metadata only. Do not allow a caller-supplied callback URL in v1. ## Resources and endpoints | Endpoint | Purpose | | --- | --- | | `POST /profiles` | Resolve/create a taxpayer profile using stable identity and optional existing credential reference; return profile and immutable revision IDs. | | `GET /profiles/{profile_id}` | Read authorized public profile metadata and active revision. | | `POST /profiles/{profile_id}/revisions` | Create a new immutable default revision, with expected current revision to prevent lost updates. | | `POST /profiles/{profile_id}/credentials` | Restricted onboarding/rotation of signing material; return an opaque credential reference, fingerprint and validation status. | | `POST /submissions` | Atomically accept a bounded batch of original, correction, cancellation or sales-return documents. | | `GET /submissions/{submission_id}` | Read receipt and aggregate counters, with link to paginated items. | | `GET /submissions/{submission_id}/items` | Cursor-paginated item IDs and current statuses. | | `GET /requests/{request_id}` | Read one document's status, public diagnostics, upstream references and state timestamps. | | `GET /requests` | Recover receipts after client-side loss; filter by profile, external ID, submission, state and creation interval. | Original/correction/cancellation/return use one admission schema with `operation`, rather than four copies of the transport contract. A correction or cancellation is a **new immutable document referring to another document**, never a PATCH or DELETE of the original. Exact eligible reference chains, fields that may change and return semantics remain gated on the current official rules. Local API status inquiry reads our durable state; it does not make an unbounded synchronous upstream call. Server-side reconciliation refreshes it on a controlled schedule. The proposed `operation` enum is `original`, `correction`, `cancellation`, `return`; “sales-return” in prose means `return`. One submission envelope resolves one taxpayer/fiscal-memory profile for all its documents. Partners batching several taxpayers create separate envelopes, which keeps authorization, receipts and failures simple. `GET` collections return `{ "items": [], "next_cursor": null }`, default page size 50, hard cap 200, stable creation-time/ID ordering and opaque cursors bound to filters and tenant. Batch status is counters, not a fictitious single upstream invoice status. Keep historical events available to operators; partner responses return a compact current view. Payment registration, fiscal-information lookup and workspace status are part of the confirmed product scope. The endpoint table currently specifies the invoice core; it is not the complete promised operation inventory. Add typed payment-operation requests with durable receipts/idempotency and separate outcomes after resolving the official method discrepancy. Workspace inquiry populates separate buyer/Article 6 observations. Public endpoints expose business meaning while nonce and server-key discovery remain internal. Track every documented capability in [the scope matrix](plan.md); portal-only operations must not be represented as supported API actions without an official interface. ## Profile resolution and credentials A profile represents an authorized taxpayer/fiscal-memory identity, not a human login. Separate organization, taxpayer, fiscal memory, profile revision and signing credential concepts even if onboarding presents one form. A taxpayer may have more than one fiscal memory. Another accounting partner must receive an explicit grant before sharing a taxpayer; never discover or merge profiles globally by national/economic ID without verified taxpayer authority and a grant. Partners retain isolated profile IDs over a shared internal fiscal-memory identity. Every submission supplies exactly one of: 1. `profile_id`, optionally `profile_revision_id` to pin a known revision. 2. Inline `profile` with partner-owned `external_profile_id`, stable taxpayer identifiers, fiscal-memory identifier and validated `credential_id`. 3. `profile_id` with `profile_overrides`, for example a newly onboarded `credential_id`. Profile resolution occurs in the admission transaction. With no pinned revision, the server locks/resolves the current default and records the exact selected revision. An override produces/reuses an immutable effective revision **for that submission only**; it does not silently change the default for future work. Explicit revision creation activates a new default after validation and optimistic concurrency checks. Already accepted requests keep their original snapshot, revision, rule version and credential reference. A revoked or unusable key blocks execution with an explicit diagnostic; it never causes automatic substitution of a different key or taxpayer. Inline identity is matched only inside the authorized partner scope using the explicit external profile ID and fiscal-memory identity. Database uniqueness and conflict checks handle simultaneous creation. An external ID reused for a different taxpayer/memory returns `409`; omitted identifiers or ambiguous matching return `422`. Metadata changes create revisions. Changing taxpayer or fiscal-memory identity requires another profile. Return the resolved `profile_id` and `profile_revision_id` in every successful receipt. Repeat complete nonsecret inline metadata is supported and resolves the same profile without multiplying revisions. Enforce one active partner profile per canonical fiscal-memory identity independently of `external_profile_id`; alternative partner aliases resolve to that same profile. Admission deduplication uses the canonical identity rather than a caller-created alias. Cross-partner grants are required now. Partner profiles remain isolated views over the same verified taxpayer/memory; sharing an identity does not grant access to another partner’s invoices or credentials. Onboarding establishes a taxpayer-controlled source namespace and source document ID for any shared document stream; independent streams retain separate namespaces. The canonical shared identity prevents duplicate admission of a document represented by multiple partners only when that source identity is coordinated. Content hashes alone cannot identify every duplicate sale. First onboarding without an existing credential is: create a non-dispatchable profile, provision its credential through the restricted endpoint, activate a validated profile revision, then submit. `POST /profiles` may omit `credential_id` for this draft onboarding state; invoice admission cannot. A partner with an already authorized credential can use inline resolution directly. Certificate/public-key metadata and private signing material are different things. A certificate alone does not supply a private key. Credential onboarding checks format, key/certificate match, supported algorithms, validity, ownership/authority and fiscal-memory association as far as the current official protocol permits. Private keys are accepted only by the restricted TLS onboarding endpoint, encrypted with a separately managed key, never echoed, and never placed in routine submission bodies. Inline submission may reference a newly onboarded credential. This deliberate two-step secret onboarding is safer than resending private keys with every invoice; clients can still inline all ordinary profile fields. Exact certificate requirements are unresolved until the official baseline is verified. ## Admission and idempotency Require `Idempotency-Key` on all mutating endpoints. This is an application-defined contract, not a claim that POST is intrinsically idempotent. Scope keys to partner, environment and route. Bind a key to a digest of the validated request representation; exclude only documented transport metadata, never invoice/profile fields. Do not normalize financial values or identifiers in a way that hides changes. Concurrent duplicate requests serialize on a unique constraint. Check a stored key and caller-supplied content before resolving the current profile default. Replaying an accepted request after credential/default rotation returns its original frozen revision. A new key referencing an existing external document identity must compare the effective immutable snapshot; a changed credential/profile snapshot is a conflict rather than a reason to resubmit an existing invoice. Authenticate and recheck current token scopes and taxpayer grants before every replay. A retained receipt never grants access to a revoked caller. Replay-before-quota/profile-resolution changes ordering of admission work, not authorization. - Same key and same request: return the original status, IDs and response; no new profile revision, job, tax number or signing operation. A concurrent request still being admitted returns `409` with `code=idempotency_in_progress` and a suggested retry delay. - Same key and changed request: `409 idempotency_conflict`. Correct the request and use a new key. - Lost response or connection timeout: retry the identical request with the identical key. A timeout does not prove rejection. - Persist a unique `(partner, canonical fiscal memory, external_id)` document identity and immutable content digest in addition to the transport key. Matching resubmissions return the existing request ID; changed content under an existing external ID returns `409`. Corrections/returns/cancellations each have a new external ID and explicit original reference. - Retain idempotency records at least as long as accepted document identity records. Proposed initial policy: no expiry of accepted-operation keys or document deduplication tombstones until the financial retention policy is approved. No silent window after which the same key can submit again. Rejected pre-admission requests may be corrected and retried; no durable accepted receipt was issued. The transaction stores profile resolution/revision, batch, documents, immutable snapshots, request identity records and dispatch intent together. Reply `202` only after commit. Perform no upstream network call in that transaction. Reject the complete batch if any item fails envelope/schema, authorization, identity/conflict, reference-shape or cheap local validation; return indexed errors and create no partial batch/profile mutation. More expensive or version-dependent checks may reject individual accepted items asynchronously, visibly as `rejected` with `stage=local`. Atomic admission does not mean atomic execution in Modian: some items can later succeed and others fail. No partial-admission mode in v1. Suggested starting bounds, configurable and subject to measurement: 100 documents per batch, 1 MiB uncompressed JSON body, 500 lines per invoice and explicit string/decimal/array limits. A receipt body remains bounded by the same item count. Reject compressed bodies initially to avoid decompression ambiguity/abuse. Publish effective limits during onboarding and in generated API docs. Enforce partner rate, pending-document and retained-byte quotas alongside system disk/database admission thresholds. `429` means a partner quota/rate limit; `503` means system/upstream-pressure admission pause; both include a retry delay where useful. Existing accepted work remains durable and queryable. Check idempotent replay before new-work quota rejection so callers can retrieve a receipt during an admission pause. For a shared document stream, require its authorized `source_namespace` and `source_document_id` and enforce a shared canonical uniqueness constraint in addition to the partner alias. Access to an existing cross-partner document must be granted before returning or linking its request; otherwise use a non-disclosing authorization/conflict response. Partners must not assume matching local invoice numbers in independent namespaces represent the same sale. The API contract will define these source fields in OpenAPI before implementation. ## Example workflow The following business fields are **illustrative**, not a complete tax-valid schema. Types/patterns, required buyer/line/payment fields, precision, rounding and mapping must come from the verified official invoice specification. Do not send this fixture to production Modian. ```http POST /api/v1/submissions Authorization: Bearer Idempotency-Key: erp-2026-000041 Content-Type: application/json ``` ```json { "profile_id": "prf_example", "profile_overrides": {"credential_id": "cred_new_example"}, "documents": [ { "external_id": "sale-1042", "operation": "original", "invoice": { "pattern": "general_sale", "issued_at": "2026-11-11T09:30:00Z", "currency": "IRR", "lines": [ { "product_service_id": "EXAMPLE_NOT_A_VALID_TAX_CODE", "quantity": "2", "unit_price": "1000000", "discount": "0" } ] } } ] } ``` Return `202 Accepted`, `Location: /api/v1/submissions/sub_example`, and a suggested polling interval. IDs below are documentation placeholders; implementation uses opaque, nonsequential public identifiers. ```json { "submission_id": "sub_example", "profile_id": "prf_example", "profile_revision_id": "prv_example", "received_at": "2026-11-11T09:30:01Z", "status_url": "/api/v1/submissions/sub_example", "items": [ { "external_id": "sale-1042", "request_id": "req_example", "state": "queued", "status_url": "/api/v1/requests/req_example" } ] } ``` A correction uses a fresh external ID and new invoice content: ```json { "profile_id": "prf_example", "documents": [ { "external_id": "correction-1042-1", "operation": "correction", "reference": {"request_id": "req_example"}, "invoice": {"pattern": "general_sale", "currency": "IRR"} } ] } ``` This second example intentionally omits required invoice fields to illustrate the reference shape only. Referenced internal requests must belong to the same authorized taxpayer/fiscal memory and be eligible under official rules. Dependencies remain blocked until upstream acceptance is proven. Do not allow a reference to bypass a rejected/unknown predecessor. For documents originally sent through another system, a separately validated `reference.tax_id` form is needed; expose it only after confirming upstream reference/inquiry rules and authority. Prevent circular dependencies and serialize changes to a document chain. ## Status contract `GET /requests/{id}` returns `request_id`, `submission_id`, `external_id`, profile/revision IDs, operation, reference, `processing`, `upstream`, `business`, `created_at` and `updated_at`. Status is an observation with timestamps, not a promise about future buyer action. | `processing.state` | Meaning | | --- | --- | | `queued` | Durable admission; eligible for scheduling or waiting on an explicit dependency. | | `preparing` | Worker holds a lease and validates/maps/signs the frozen snapshot. | | `submitting` | Attempt recorded before the external call; result not yet durably classified. | | `awaiting_result` | Upstream receipt exists; final registration outcome is pending inquiry. | | `retry_wait` | A safely retryable operation is scheduled at `next_attempt_at`. | | `outcome_unknown` | The upstream may have received the request; reconcile before any resend. | | `needs_attention` | Automated progress cannot continue safely; operator action and alert required. | | `succeeded` | Verified upstream registration acceptance, with evidence/reference recorded. | | `rejected` | Definitive local or upstream rejection; `stage` and stable reason identify which. | `upstream` separately exposes validated tax ID when allocated, receipt/reference identifiers when supplied by Modian, mapped registration outcome, safe raw status code and `last_checked_at`. `business` represents buyer/transaction lifecycle observations separately; use `unknown` when not supported or not observed. Never translate transport HTTP 200, queue receipt or buyer nonresponse into registration success. Never invent an upstream receipt or infer acceptance from an empty inquiry. An unsupported/ambiguous upstream result moves to visible reconciliation/attention. Ordinary pending upstream results do not block unrelated documents in the same fiscal-memory lane once a durable receipt exists; `outcome_unknown` conservatively pauses new submissions in that lane until reconciliation, and dependencies always wait for proven eligibility. If uncertainty escalates to `needs_attention`, its lane remains blocked until an evidence-backed resolution. A later new document may change the business effect of an earlier accepted invoice; preserve its historical registration result. Queries return local state and `next_check_at`/`next_attempt_at` when applicable. Poll using the recommended interval, exponential backoff and jitter; no per-invoice tight loops. Batch counters include state counts, total and update time. Operators have explicit evidence-preserving repair actions; partners do not get a generic blind “retry send” endpoint. v1 polling is sufficient; signed webhook delivery is a later feature with its own durable outbox, retry and SSRF design. ## Types, errors and compatibility Identifiers, national/economic IDs and fiscal-memory codes are strings; leading zeros are significant. Financial and quantity inputs are decimal strings with bounded scale, parsed using decimal arithmetic. No IEEE-754 floats and no implicit toman conversion. Ordinary Rial amounts use IRR explicitly; in-scope currency/export patterns have separately specified foreign-currency values, units and conversion fields derived from their official schemas. Validate provided totals against documented rounding rules; do not silently change invoice economics. Store instant timestamps in UTC and accept RFC 3339 with an explicit offset; emit UTC `Z` ([RFC 3339 §5.6](https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6)). Reject naive timestamps. Date-only tax fields remain dates rather than midnight timestamps. Jalali entry/display and `Asia/Tehran` belong to the Persian interface; the adapter performs only documented upstream date/epoch conversions. Do not hardcode historic DST offsets. Use UTF-8 JSON. Machine identifiers and decimal syntax use ASCII digits; reject malformed identifier characters rather than silently rewriting signed business content. The Persian UI may normalize Arabic/Persian forms for search and display before an explicitly confirmed payload is created. RTL layout must keep identifiers, decimal values and code examples readable in LTR spans. Errors use `application/problem+json`, with `type`, `title`, `status`, safe `detail`, request correlation ID, stable `code` and optional indexed `errors` entries with JSON pointers ([RFC 9457 §3](https://www.rfc-editor.org/rfc/rfc9457.html#section-3)). Field errors must not echo secret values. English codes remain stable; localized Persian explanatory text may evolve. Upstream rejection after admission is returned in the status resource, not as an HTTP failure for a successful status lookup. | HTTP | Contract | | --- | --- | | `200` | Read/replay of a synchronous success. | | `201` | Profile/revision/credential created synchronously. | | `202` | Submission admitted, including exact accepted-submission replays. | | `400` | Invalid JSON or request envelope. | | `401` / `403` | Missing/invalid credential or insufficient permitted scope. | | `404` | Object not available in caller's authorized scope. | | `409` | Idempotency, document identity or profile revision conflict. | | `413` / `415` | Payload limit exceeded or unsupported media/encoding. | | `422` | Schema/business input validation failed before admission. | | `429` / `503` | Capacity/rate admission control; receipt lookup remains available. | ```json { "type": "https://tax.didis.ir/problems/validation", "title": "Request validation failed", "status": 422, "code": "validation_error", "correlation_id": "corr_example", "errors": [ {"pointer": "/documents/0/invoice/issued_at", "code": "timezone_required"} ] } ``` Publish one generated, version-controlled OpenAPI 3.1 contract with request/response examples and executable contract tests when implementation begins; this prose defines decisions, not a competing hand-maintained schema ([OpenAPI 3.1.1](https://spec.openapis.org/oas/v3.1.1.html)). Select the exact compatible 3.1 revision with implementation tooling. `/v1` is our public API version, independent of upstream API version, adapter release and invoice-rule version. Unknown input fields fail validation initially to catch misspellings; consumers must tolerate additive response fields. Breaking semantic or required-field changes need a new contract version and migration/deprecation policy. New tax patterns require explicit support declarations and fixtures, not a raw arbitrary upstream-payload escape hatch. The [current official invoice research](research/modian.md) identifies truncation at field-specific precision, not a universal half-up rule. It also identifies sales-return documents as the remaining invoice contents rather than generic negative lines. These rules need explicit partner-facing schemas and golden examples before `return` or monetary validation ships; the illustrative payloads above deliberately do not define those calculations. ## Foundation authorization semantics Partner API tokens and taxpayer signing credentials are separate database-backed concepts. Tokens are verified from stored verifiers; taxpayer signing material is encrypted and accessible only through authorized use grants. Initial grants are staff-verified until taxpayer self-service is implemented. Profile creation alone grants no signing authority. Revoking an API token blocks its access without canceling previously accepted jobs; revoking underlying taxpayer/partner authority blocks new side effects visibly. Reconciliation of existing attempts continues only where remaining taxpayer authority permits it. See [foundation decisions](architecture.md#foundation-decisions-confirmed-2026-11-12). ## Implemented foundation surface — 2026-11-12 `GET /api/v1/me` reports the authenticated partner ID/name, token scopes and expiry; it never returns the token or signing credentials. `GET /api/v1/profiles` and `GET /api/v1/profiles/{uuid}` require `profiles:read` and an active taxpayer `inquire` grant, and expose only the caller's profiles. Revocation is checked on every read. The list emits `items` and a signed opaque `next_cursor`, bound to the partner and valid for 24 hours. Supply it as `?cursor=...`; `page_size` defaults to 50, capped at 200. No arbitrary filters are implemented yet. Timestamps emit UTC `Z`. Current profile fields are `id`, `external_profile_id`, `active_revision_number`, `created_at`; detail adds `revision_id` and whitelisted ordinary metadata. A metadata revision is not a verified/dispatchable taxpayer credential configuration. Persisted profile aliases preserve external-ID conflict checks across fiscal memories. Future mutations and invoice/credential endpoints in this document remain unimplemented; no simulated receipt is exposed as a live submission result. The [generated schema](api-schema.json) describes only the implemented routes; CI validates it and checks regeneration for drift. ## Public preview availability `https://tax.didis.ir/docs/` publishes this contract for review. `/api/v1/schema.json` serves the current machine-readable schema; `/status.json` describes public availability. Other public `/api/` routes currently return `503 api_not_enabled` with retry guidance. Implemented backend read routes remain accessible only in isolated development until partner execution is enabled. Internal encrypted credential and simulation services are not HTTP endpoints and cannot register an invoice. Use this preview for contract review, not production integration or credential submission.