> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tangoanalytics.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Send your first request to Tango in under five minutes &mdash; submit a contracts batch, poll for status, handle the response.

This guide walks you through a complete request / response cycle against the **Contracts Inbound API** — one of the simplest async integrations to start with. By the end you'll have submitted a batch, polled its status, and parsed both the success and error shapes.

The same pattern (auth headers + async submission + `gu_id` polling) applies to every REST API integration in this docs site, so what you learn here transfers directly to AP, AR, GL, Lease, Sales, and the rest.

<Note>
  Need SFTP file-based integration instead? Jump to the [SFTP file delivery](#alternative-sftp-file-delivery) section at the bottom.
</Note>

## Prerequisites

<Steps>
  <Step title="Get your API credentials">
    Contact your Tango onboarding lead to request a client identifier and API key. You'll receive two values:

    * `x-client` — a string identifying your client.
    * `x-api-key` — a secret bound to that client.

    Both are needed on every request. Keep `x-api-key` in a secret manager, never in source control.

    <Warning>
      The test (`testapi.tangoanalytics.com`) and production (`api.tangoanalytics.com`) keys are different. Don't reuse the same value across environments.
    </Warning>
  </Step>

  <Step title="Confirm a supplier site exists">
    The Contracts Inbound API upserts on the key `(supplier_site_number, contract_number)`. Your `supplier_site_number` must already exist in Tango — if it doesn't, submission will fail with `supplier_site_number does not exist`.

    Ask your Tango onboarding lead which supplier site numbers are pre-loaded in your test environment, or pick one from the data your team has already migrated.
  </Step>

  <Step title="Pick an environment">
    Use the test environment for everything in this guide:

    ```bash theme={null}
    export TANGO_BASE="https://testapi.tangoanalytics.com"
    export TANGO_CLIENT="<your-x-client-value>"
    export TANGO_API_KEY="<your-x-api-key-value>"
    ```

    For production, swap the base URL to `https://api.tangoanalytics.com` and the credentials to your prod values.
  </Step>
</Steps>

## Submit your first batch

The Contracts Inbound endpoint accepts a JSON body with a `records` array. The minimum required fields per record are: `contract_number`, `contract_name`, `supplier_site_number`, `contract_start_date`, `contract_end_date`, `contract_amount`.

<CodeGroup>
  ```bash curl theme={null}
  curl -X PUT "$TANGO_BASE/v1/suppliermgmt/batch/agreements" \
    -H "x-client: $TANGO_CLIENT" \
    -H "x-api-key: $TANGO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "records": [
        {
          "contract_number": "QUICKSTART-001",
          "contract_name": "Quickstart Demo Contract",
          "supplier_site_number": "670234-1",
          "contract_start_date": "06/11/2013",
          "contract_end_date": "03/20/2026",
          "contract_amount": 5000000,
          "modification_amount": 70241015.18,
          "contract_status": "A"
        }
      ]
    }'
  ```

  ```python python theme={null}
  import os, requests

  base    = os.environ["TANGO_BASE"]
  headers = {
      "x-client":     os.environ["TANGO_CLIENT"],
      "x-api-key":    os.environ["TANGO_API_KEY"],
      "Content-Type": "application/json",
  }

  payload = {
      "records": [
          {
              "contract_number":      "QUICKSTART-001",
              "contract_name":        "Quickstart Demo Contract",
              "supplier_site_number": "670234-1",
              "contract_start_date":  "06/11/2013",
              "contract_end_date":    "03/20/2026",
              "contract_amount":      5_000_000,
              "modification_amount":  70_241_015.18,
              "contract_status":      "A",
          }
      ]
  }

  r = requests.put(f"{base}/v1/suppliermgmt/batch/agreements",
                   headers=headers, json=payload, timeout=30)
  r.raise_for_status()
  gu_id = r.json()["gu_id"]
  print("Submitted; gu_id =", gu_id)
  ```

  ```javascript node.js theme={null}
  import fetch from "node-fetch";

  const base = process.env.TANGO_BASE;
  const headers = {
    "x-client":     process.env.TANGO_CLIENT,
    "x-api-key":    process.env.TANGO_API_KEY,
    "Content-Type": "application/json",
  };

  const payload = {
    records: [{
      contract_number:      "QUICKSTART-001",
      contract_name:        "Quickstart Demo Contract",
      supplier_site_number: "670234-1",
      contract_start_date:  "06/11/2013",
      contract_end_date:    "03/20/2026",
      contract_amount:      5_000_000,
      modification_amount:  70_241_015.18,
      contract_status:      "A",
    }],
  };

  const res = await fetch(`${base}/v1/suppliermgmt/batch/agreements`, {
    method:  "PUT",
    headers,
    body:    JSON.stringify(payload),
  });
  const { gu_id } = await res.json();
  console.log("Submitted; gu_id =", gu_id);
  ```
</CodeGroup>

You'll get back an immediate acknowledgement:

```json theme={null}
{
  "status": "OK",
  "status_message": "",
  "gu_id": "721c54c4-c7aa-4fa1-8fdfd6-c0cee161694d"
}
```

<Info>
  Hang onto that `gu_id` — you'll use it in the next step. It uniquely identifies the batch and can be polled repeatedly. If you lose it you cannot retrieve the result; you'd need to resubmit (which would also overwrite, since the upsert key is `(supplier_site_number, contract_number)`).
</Info>

## Poll for status

Tango processes the batch asynchronously. Hit the shared status endpoint until you get a terminal state (`COMPLETED` or `ERROR`):

<CodeGroup>
  ```bash curl theme={null}
  curl "$TANGO_BASE/v1/batch/status?gu_id=$GU_ID" \
    -H "x-client: $TANGO_CLIENT" \
    -H "x-api-key: $TANGO_API_KEY"
  ```

  ```python python theme={null}
  import time, requests

  while True:
      r = requests.get(f"{base}/v1/batch/status",
                       params={"gu_id": gu_id},
                       headers=headers, timeout=30)
      body = r.json()
      print(body)
      if body["status"] != "PROCESSING":
          break
      time.sleep(10)
  ```

  ```javascript node.js theme={null}
  async function pollUntilDone(guid) {
    while (true) {
      const res  = await fetch(`${base}/v1/batch/status?gu_id=${guid}`, { headers });
      const body = await res.json();
      console.log(body);
      if (body.status !== "PROCESSING") return body;
      await new Promise(r => setTimeout(r, 10_000));
    }
  }
  const finalResult = await pollUntilDone(gu_id);
  ```
</CodeGroup>

You'll see one of three response shapes, in this order:

<Tabs>
  <Tab title="PROCESSING">
    ```json theme={null}
    { "status": "PROCESSING" }
    ```

    Keep polling. There's no time guarantee — small batches typically clear within seconds; multi-thousand-record submissions can take a few minutes.
  </Tab>

  <Tab title="COMPLETED">
    ```json theme={null}
    {
      "status": "COMPLETED",
      "total_records": 1,
      "errors": null
    }
    ```

    All records were applied. You're done.
  </Tab>

  <Tab title="ERROR (partial)">
    ```json theme={null}
    {
      "status": "ERROR",
      "total_records": 1,
      "errors": [
        {
          "record_index": 0,
          "record_key": "QUICKSTART-001",
          "error": "supplier_site_number does not exist"
        }
      ]
    }
    ```

    Some (or all) records failed validation. The `errors` array has one entry per failed record, with `record_index` pointing back to the position in your original `records` array and `error` containing a comma-separated list of error messages for that record.

    Records that aren't listed in `errors` succeeded.
  </Tab>
</Tabs>

<Tip>
  **Polling cadence.** Start at 5–10 seconds, back off to 30–60 seconds after a minute, and cap at 2 minutes. Don't poll faster than every 5 seconds; it doesn't make the batch finish sooner.
</Tip>

## Handle errors properly

A few error patterns to watch for:

<AccordionGroup>
  <Accordion title="HTTP 401 / 403 on submission" icon="lock">
    Your `x-client` / `x-api-key` headers are missing, mistyped, or don't have access to the endpoint. Verify both values and confirm with Tango that your client has the right permissions enabled.
  </Accordion>

  <Accordion title="HTTP 400 with \`status: Error\` on submission" icon="triangle-alert">
    The submission itself was rejected before queueing — usually a malformed JSON body or a content-type mismatch. The `status_message` field will tell you which. A `gu_id` is NOT returned for these.
  </Accordion>

  <Accordion title="\`status: ERROR\` on status poll with empty errors\[\]" icon="circle-question">
    Tango couldn't process the batch at all (rare). Check `status_message` for context; resubmit the batch.
  </Accordion>

  <Accordion title="Per-record validation failures" icon="list-x">
    See the Validations table in the per-integration `.md` reference for the full list of error messages. Common ones:

    * `<field> is missing or empty` — required field omitted.
    * `<field> does not match expected format` — usually a date in something other than `MM/DD/YYYY`.
    * `supplier_site_number does not exist` — referenced an unknown entity.
    * `<field> does not match lookup value` — sent a value outside the configured lookup set.
  </Accordion>
</AccordionGroup>

## Where to go next

<CardGroup cols={2}>
  <Card title="Browse all integrations" icon="list" href="/integrations">
    See every available integration grouped by domain.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference">
    The full OpenAPI 3.1 spec catalogue.
  </Card>

  <Card title="Async patterns deep dive" icon="clock" href="/concepts/async">
    More on `gu_id`, pagination, retries, and idempotency.
  </Card>

  <Card title="Field conventions" icon="book" href="/concepts/conventions">
    Date formats, null-omission rules, currency handling.
  </Card>
</CardGroup>

## Alternative: SFTP file delivery

If you're integrating via SFTP files instead of REST APIs, the flow is different but simpler:

<Steps>
  <Step title="Get SFTP credentials">
    Tango will provision a username + password (or SSH key) for the relevant SFTP host:

    * Production: `sftp.tangoanalytics.com`
    * Test (SIT/UAT): `sftpdev.tangoanalytics.com`
  </Step>

  <Step title="Drop a pipe-delimited \`.txt\` file">
    Use the integration's documented path and naming pattern, e.g.:

    ```text theme={null}
    /inbound/ap/MyClient_apin_20260515093000.txt
    ```

    Body is one record per line, fields separated by `|`. See the integration's `.md` reference for the exact column order.
  </Step>

  <Step title="Wait for Tango to consume the file">
    Tango's batch program picks up files on a pre-agreed schedule. Once consumed, the file moves to a `Processed/` subfolder with a timestamp suffix. Any errors are written to a service log and emailed to the configured contact.
  </Step>
</Steps>

<Note>
  Most file-based integrations have a REST API counterpart that's preferred for new work. Check the integrations list to see which mode is appropriate for your use case.
</Note>
