Skip to main content

Overview

Platform payouts is for marketplaces and digital platforms that pay providers under the SAT digital-platforms scheme (Plataformas Tecnológicas: RESICO régimen 625, retention key 26). Ride-hailing drivers, delivery couriers, lodging hosts and sellers of goods are all providers. The platform issues the CFDIs for a whole payout period from two files:
  • a movements file, with one row per payout to a provider;
  • a commissions file, with one row per provider per month holding the platform commission.
gigstack matches every row to the provider’s gigstack team in your billing account and computes a plan. Once you confirm the plan, it stamps every document in the background.
What it is not. This API is for a platform that invoices on behalf of its providers. If your company invoices its own sales, one CFDI per sale, use POST /invoices/income instead.
Base path: https://api.gigstack.io/v2/platform-payouts A run goes through three steps:
  1. Plan. POST /platform-payouts uploads both files. The plan is computed during the request and returned in the response. Nothing is stamped yet.
  2. Review. GET /platform-payouts/{id} returns the totals. GET /platform-payouts/{id}/movements lists every row with what will be issued for it, or why nothing will be.
  3. Confirm. POST /platform-payouts/{id}/confirm hands the plan to the stamping worker. This cannot be undone. Poll GET /platform-payouts/{id} until the run is completed or failed, then read its result to see how it went.

Key Features

  • Three documents per payout period - the provider’s income invoice, your retention certificate and your monthly commission invoice
  • Tax policy per account - the regimes, SAT keys and withholding rates come from your account’s configuration (see Tax Policy)
  • Synchronous planning - the plan, or the reason it failed, is in the response to the upload
  • Idempotent uploads - a required Idempotency-Key makes a retried upload return the same run. If the key comes back with different files, it is refused
  • Row-level exclusions - a malformed or ineligible row is excluded with a reason and a stable code, and the other rows still go ahead
  • A clear outcome - a finished run reports result: completed, partially_completed or failed
  • No double certification - a provider-month already certified by an earlier run is skipped
  • Safe confirm - confirming twice returns the run’s status and does nothing else

Who Can Use It

Platform payouts is for marketplace master teams. All of these must hold: On POST /platform-payouts, these checks (and team_not_found / not_a_member) run right after the Idempotency-Key check and before the upload is read. The files of a refused request are never processed or stored. Credentials:
  • API keys and OAuth access tokens belong to the team. They can create, read and confirm that team’s runs.
  • User-scoped tokens (MCP tokens, dashboard sessions) act as a person, who must be a member of the team (403 forbidden / not_a_member otherwise). The two POST routes also require editor permission on invoices (403 forbidden, This action requires editor access to invoices).
  • gigstack Connect doesn’t apply. Runs always act on the credential’s own master team. Sending ?team=<connected team> moves the request to a team that isn’t a master team. Uploads and confirms are then refused with not_master_team, and reads return 404.
The mode comes from the credential only. A test key creates and sees test runs, and a live key creates and sees live runs. A run of another team, or of the other mode, returns 404 run_not_found, never 403, so an id can’t be used to discover someone else’s run.

What Gets Issued

For each movement (payout) and each provider-month, the plan decides which of three comprobantes to issue: The monthly commission is prorated across the provider’s movements of that month, in proportion to each subtotal. That share is the movement’s commission, and it is what the certificate reports. The commission invoice bills the full monthly figure. Which documents a provider qualifies for depends on your account’s tax policy. Rows that don’t qualify are excluded with a Spanish reason and a stable code. See Exclusion codes.

Tax Policy

The documents a run issues, and the SAT keys and rates they carry, are set per master account when the account is onboarded. The same files can produce different CFDIs for a delivery platform and for a ride-hailing platform. The policy covers:
  • Eligibility: which tax regimes providers may be on, whether a provider needs a valid CSD (sellos) in gigstack, and whether a certificate may be issued to público en general (the generic RFC).
  • Retention certificate: the service type (tipoDeServ) and subtype (subTipServ) of the digital-platforms complement, and the ISR and IVA withholding rates.
  • Invoices: the product keys of the income and commission invoices, the unit, the concept descriptions, and the CFDI use, payment form, payment method and currency.
If your account has no specific configuration, the defaults apply. The defaults are set for ground passenger transport, so check them before your first live run: If your providers deliver goods, host lodging or provide another kind of service, contact support to configure your policy before your first live run. The policy can’t be changed through the API.

Input Files

Both files are sent in one multipart/form-data request: Rules for both:
  • The format is chosen by extension (the MIME type is ignored). .csv and .txt are read as comma-separated text (UTF-8, a BOM is fine). .xlsx, .xls and .xlsm are read as workbooks, and only the first sheet is used.
  • Max 5 MB per file (413 file_too_large) and max 50,000 rows (the plan fails).
  • The first row is the header. Headers match regardless of case, accents and repeated spaces (CORREO ELECTRONICO matches Correo electrónico). Extra columns are ignored.
  • Amounts are in MXN. 1250.00, 1,250.00, 1250,5 and $1,250.00 are all read. Any other value is a row problem, never a silent zero.
  • Column names: each column below has a primary name and a few accepted alternatives. Use the primary name in new files. When a column is missing, the error names it by its primary name.

Provider columns (both files)

When a file has more than one name column, the more specific one wins: Nombre del proveedor or Nombre del conductor, then Razón social, then a bare Nombre.

Movements file columns

The provider columns, plus:

Commissions file columns

The provider columns, plus: Commissions are joined to movements on the provider id and the month. The commission invoice is excluded if the row has no RFC. If a provider-month appears more than once, the first row is used. A certificate needs a commission. The SAT rejects a retention certificate whose commission is zero (SPT147). A movement whose provider-month has no commission in the commissions file therefore gets no certificate.

File problems and row problems

  • A file problem fails the whole plan. The run comes back as plan_failed with a Spanish error. Examples: the file can’t be read, it’s empty, it has more than 50,000 rows, or required columns are missing (all missing columns are listed at once, for example Al archivo de comisiones le faltan estas columnas: Comisión.).
  • A row problem (an invalid date or amount, a subtotal below 0.01) only excludes that movement, with the problem as its reason.

Matching providers

Each row is matched to a team in your billing account. gigstack tries the provider id first (the team’s metadata.driverId), then the RFC, then the e-mail. When an RFC or e-mail matches more than one team, the row is excluded rather than guessed.

Run Status

Run Result

status: "completed" only means the worker has nothing left to try. To know how a finished run went, read result, not status. It is null until the run finishes (completed or failed). How it’s counted:
  • Failures are progress.failed_count plus progress.commission_failed_count. A movement counts as failed as soon as one of its documents (income invoice or certificate) failed. Commission invoices are counted separately, because failed_count counts movements only.
  • Issued documents are income_invoices_count + certificates_count + commission_invoices_count.
Runs that finished before result existed get it derived from their counters by the same rule.

Endpoints

Create a Run

Uploads both files and returns the planned run. Headers: Form fields: movements_file and commissions_file, both required. See Input Files.
Response (201):
A plan that failed on the file is also a 201, with status: "plan_failed", zero counts and the reason in error:

Idempotency

The run id is derived from your team, the credential’s mode and the Idempotency-Key, and the run records a fingerprint of the two files’ contents (their bytes; the file names don’t count). So:
  • The first request with a key creates the run: 201.
  • A later request with the same key and the same files returns that run, in whatever status it is now: 200. Nothing is planned again.
  • The same key with different files is refused: 409 idempotency_key_reused. The new files aren’t planned; send them under a new key. Renaming a file doesn’t change the fingerprint, and changing a single byte does.
  • Runs created before the fingerprint existed have none. A later request with their key is answered as before, 200 with the existing run, whatever files it carries.
  • A retry that arrives while the first request is still planning gets the run in planning. Poll GET /platform-payouts/{id}; don’t upload again.
  • A plan that failed stays failed under its key. Fix the file and upload with a new key.
  • The same key used with a test key and with a live key names two different runs.
If a run is stuck in planning for more than 10 minutes (the instance planning it died), a retry with the same key takes it over and plans it again from the files in that retry. Use one key per distinct upload, for example payouts-2026-08-v1, then payouts-2026-08-v2 after a fix.

Get a Run

Returns the run in the same shape as the create response. After confirming, progress fills in as the worker stamps, and result is set once the run finishes.
A finished run in which a few documents failed:

List Movements

One entry per row of the movements file, in file order, with the state of its income invoice and retention certificate. Note the nested shape: the array is at data.data and the cursor at data.next. Keep requesting with next while data.has_more is true. The cursor stays valid while the worker updates statuses, so pages never overlap or skip rows.
The third movement had only its certificate planned (the provider’s team has no invoice series, so the income invoice is skipped with reason_code: "missing_series"). A movement is excluded only when neither document is planned; then exclusion_codes lists the distinct codes of both. Commission invoices are not listed per provider-month; the run only reports how many were planned (planned_documents.commission), stamped (progress.commission_invoices_count) and failed (progress.commission_failed_count).

Confirm a Run

Irreversible. Confirming hands the plan to the stamping worker. A stamped CFDI can only be cancelled, not undone. Review the plan first.
No request body. Answers as soon as the run is handed over; the stamping itself happens in the background (the worker runs every minute).
  • Safe to retry. Confirming a run that is already stamping or completed returns 200 with its current status and does nothing else.
  • Only a plan_ready run with at least one planned document can be confirmed. Otherwise: 409 plan_not_ready (still planning), 409 not_confirmable (plan_failed or failed), 409 nothing_to_stamp (every document was excluded).
  • The entitlement is checked again: if platform payouts was disabled on the billing account since the upload, the confirm is refused with 403 feature_disabled.

Provider-months already certified

A provider’s monthly commission is prorated over the movements of this file. If an earlier run already certified the same provider and month, certifying it again would report the platform commission twice to the SAT. So, when you confirm, the run reserves every provider-month it certifies:
  • A provider-month held by an earlier confirmed run of your team (same mode) is given up: those movements’ certificate becomes skipped, with reason_code: "certificate_month_reserved" and a reason naming the earlier run (La corrida batchrun_… ya reservó las constancias de 2026-08 para este proveedor).
  • The run’s included_count, excluded_count, exclusion_summary, exclusion_code_summary and planned_documents.certificate are recomputed to match. A movement left with nothing to issue becomes excluded, with exclusion_codes: ["certificate_month_reserved"].
  • Income and commission invoices are not affected.
Provider-months that an earlier run had already reserved when this one was planned are excluded at planning time instead, with the same code and the reason La corrida … ya emitió constancias de … para este proveedor: vuelve a subir el mes completo o emítelas por separado. To certify a month correctly, upload all of the provider’s movements for that month in one run.

Following progress

Poll GET /platform-payouts/{id} every 30-60 seconds. The worker stamps one document at a time, with a short pause between stamps, so a run of several hundred movements takes several minutes or more.
  • progress.stamped_count / progress.failed_count count movements.
  • progress.income_invoices_count, certificates_count and commission_invoices_count count comprobantes; commission_failed_count counts failed commission invoices. income_invoices_amount is the total of the stamped income invoices in MXN.
  • When the run finishes, read result. If it is partially_completed or failed, list the movements and look for status: "failed", the document’s error (Spanish, for people) and its error_code (for programs).
  • A transient failure (for example the PAC being unavailable, error_code NETWORK_ERROR or HTTP_503) is retried, up to 3 attempts per document; while it waits, the document stays planned with the last error and error_code. A SAT rejection is final; its error_code is the CFDI error code (for example STAMPING_ERROR).
  • A stamp that was sent to the PAC but never recorded fails with error_code: "interrupted_stamp" and Timbrado interrumpido: verificar en el PAC antes de reintentar. It is not retried automatically, because retrying could issue a second CFDI for the same payout. Contact support.
  • If a provider’s team was moved out of your billing account, or scheduled for deletion, after the plan was made, its documents fail with error_code: "team_out_of_scope" and nothing is issued for it (La cuenta del proveedor ya no pertenece a esta cuenta de facturación: no se emitió nada. or La cuenta del proveedor está programada para eliminarse: no se emitió nada.). If the team was deleted, the error is La cuenta del proveedor ya no existe en Gigstack.
Finding the stamped documents. Each document’s uuid is the SAT folio fiscal. For a certificate, invoice_id is the retention’s id, readable with GET /retentions/{id}. For income, invoice_id is an invoice of the provider’s team (provider.team), not of yours; read it with the team parameter if your plan includes gigstack Connect.

End-to-End Example

Use Bash, curl and jq, with GIGSTACK_API_KEY loaded locally and the input files reviewed for the intended team and mode. These scripts do not retry failed writes automatically. An HTTP or JSON error stops the script; reconcile any ambiguous write before starting again.
Review before proceeding: check totals, provider identity, every exclusion and planned fiscal document. Confirming starts fiscal issuance. Run the next block only for the intended reviewed run, in the same shell (or restore its RUN_ID and BASE). A plan_failed, empty or unexpected plan is not ready to confirm.

Response Objects

Run

Movement

Document state (income, certificate)

Exclusion codes

Every reason a document isn’t planned comes with a stable code: reason_code on the document, exclusion_codes on an excluded movement, and the keys of the run’s exclusion_code_summary. Branch on the codes. Codes are never renamed (new ones may be added); the Spanish sentences in reason, exclusion_reason and exclusion_summary are for people and may be reworded at any time.

Error Handling

All errors use the standardized envelope. Messages are in Spanish (a few validation messages are in English); branch on error.code, not on the message.
Authentication failures (401, and the 403 for a revoked key or a plan without API access) come from the authentication layer with a raw { "message": … } body; see Authentication errors. A problem inside a file isn’t an HTTP error: the upload answers 201 with status: "plan_failed" and the reason in data.error.

Best Practices

  1. Use one Idempotency-Key per upload and reuse it only to retry that same upload (same file contents). After fixing a file, use a new key; reusing the old one is 409 idempotency_key_reused.
  2. Review before confirming. Check exclusion_code_summary and list the excluded movements; fix provider data in gigstack or the files and upload again rather than confirming a plan with surprises.
  3. Upload whole months. Certificates prorate the monthly commission over the movements in the file. A provider-month split across runs is only certified by the first.
  4. Include the commissions for every month in the movements file, or those movements get no certificate.
  5. Try it with a test key first. Test runs are separate from live runs.
  6. Poll gently. Every 30-60 seconds is enough; the worker runs once a minute.
  7. Read result, not status, when the run finishes. On partially_completed or failed, list the failed movements and branch on each document’s error_code.
  8. Branch on codes, not sentences: error.code, reason_code, exclusion_codes, error_code.

For additional assistance, contact support@gigstack.io