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

# Submit candidates

> Upload resumes, bulk-submit candidates to your talent network, and poll for per-candidate outcomes.

Submitting is a three-step flow: upload each resume, submit the candidates that reference those uploads, then poll the batch until every candidate has a final outcome.

<Info>
  Requires an agency key with `talent_network:write` (and `talent_network:read` to poll). Your agency must have **Talent Network management** enabled. Without it, both submit endpoints answer `403` (`permission_error`).
</Info>

<Steps>
  <Step title="Upload each resume">
    [`POST /resumes`](/agency/api-reference/upload-resume) takes the PDF as raw base64 (no `data:` prefix), up to 8 MB decoded, and returns a `resumeFileId`.

    ```bash theme={null}
    curl -X POST "https://external-api.paraform.com/api/external/v1/resumes" \
      -H "Authorization: Bearer $PARAFORM_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d "{\"fileName\":\"jane-doe.pdf\",\"contentBase64\":\"$(base64 -i jane-doe.pdf)\"}"
    ```

    ```json theme={null}
    { "resumeFileId": "8c1d2f4e-6a7b-4c3d-9e2f-1a0b3c4d5e6f" }
    ```

    Only your agency can reference a `resumeFileId`. Uploads that are never referenced may eventually be discarded.
  </Step>

  <Step title="Submit up to 100 candidates">
    [`POST /talent-network/candidates/submit-bulk`](/agency/api-reference/submit-talent-network-candidates-bulk) takes an `items` array of 1 to 100 candidates.

    ```bash theme={null}
    curl -X POST "https://external-api.paraform.com/api/external/v1/talent-network/candidates/submit-bulk" \
      -H "Authorization: Bearer $PARAFORM_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: $(uuidgen)" \
      -d '{
        "items": [
          {
            "name": "Jane Doe",
            "email": "jane@example.com",
            "linkedinUrl": "https://www.linkedin.com/in/janedoe",
            "resume": { "resumeFileId": "8c1d2f4e-6a7b-4c3d-9e2f-1a0b3c4d5e6f" },
            "onMarketConfirmed": true,
            "clientReferenceId": "crm-48213",
            "preferences": {
              "locations": ["san_francisco"],
              "workplaceTypes": ["HYBRID"],
              "salaryMin": 180000,
              "idealFundingRounds": ["SERIES_A", "SERIES_B"],
              "visaAuthorization": "NO_VISA_AUTHORIZATION_NEEDED"
            }
          }
        ]
      }'
    ```

    Each item needs:

    | Field | Notes |
    | - | - |
    | `name`, `email`, `linkedinUrl` | Required. |
    | `resume.resumeFileId` | Required. From step 1, uploaded by your agency. |
    | `preferences` | Required. See [Candidate preferences](/agency/guides/candidate-preferences) for the required fields. |
    | `onMarketConfirmed` | Required, must be `true`. Your attestation that the candidate is actively on the market. |
    | `clientReferenceId` | Optional. Echoed back verbatim so you can match results to your CRM rows. |
    | `recruiterEmail` | Optional. Attributes the candidate to that agency member. Omitted means the agency owner. An unknown, non-member, or unapproved email rejects the item. It never silently falls back to the owner. |

    The response is always `200`, with a `batchId` and an `items` array aligned by index with your request:

    ```json theme={null}
    {
      "batchId": "0e6a1b2c-3d4e-4f50-8a9b-0c1d2e3f4a5b",
      "items": [
        { "index": 0, "clientReferenceId": "crm-48213", "status": "queued", "submissionId": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d" }
      ]
    }
    ```

    | `status` | Meaning |
    | - | - |
    | `queued` | Accepted for ingestion. Track it in the batch. |
    | `rejected` | Failed validation. `errors[]` lists `field`, `code`, and `message`. Fix the item and resubmit it. |
    | `deduplicated` | An identical submission of the same candidate is still in progress. |

    Rejection codes: `missing_required_field`, `invalid_value`, `unknown_resume`, `recruiter_not_found`, `recruiter_not_approved`, `agency_owner_unavailable`, `enqueue_failed`.
  </Step>

  <Step title="Poll the batch">
    [`GET /talent-network/batches/{batchId}`](/agency/api-reference/get-talent-network-batch) returns the current outcome of each queued item.

    ```bash theme={null}
    curl "https://external-api.paraform.com/api/external/v1/talent-network/batches/$BATCH_ID" \
      -H "Authorization: Bearer $PARAFORM_API_KEY"
    ```

    Items move from `queued` to `processing`, then to `succeeded` (with `candidateUserId`) or `failed` (with `errorCode` and `errorMessage`). `deduplicated` is final.

    Processing includes LinkedIn profile verification and can take tens of seconds per candidate. Poll every 30 to 60 seconds, or less often for large batches, until every item is `succeeded`, `failed`, or `deduplicated`. An item that never completes is failed automatically with `ingest_timed_out` a few hours after submission.
  </Step>
</Steps>

## Reading batch results

* **Index gaps are expected.** Items rejected at submit time get no row in the batch, so `itemCount - items.length` is the number of request-time rejections. The exception is `enqueue_failed`, which does appear here as `failed`.
* **`succeeded` means listed.** The candidate is in your talent network as soon as the item succeeds. Rarely, enrichment is retried and the candidate appears shortly after.
* **`failed` is always resubmittable.** Some failures are transient: `resume_parse_failed`, `linkedin_verification_unavailable`, and `internal_error`. `enqueue_failed` and `ingest_timed_out` are service-side problems. Resubmitting later should work for all of these. For any other code, the problem is the candidate's data. Fix the data first.
* **Unknown codes are terminal.** The full list of `errorCode` values is on the [batch reference page](/agency/api-reference/get-talent-network-batch). Codes are never renamed, but new ones may be added.

## Retries and duplicates

* A candidate comes back `deduplicated` only while an earlier submission of the same candidate (same attributed recruiter, email, and LinkedIn URL) is still `queued` or `processing`. Once that earlier submission finishes, resubmitting starts a fresh attempt.
* Because attribution is part of that match, a retry must send the same `recruiterEmail`.
* Items for the same person within one request collapse onto the first one. Later items come back `deduplicated`.
* With an [`Idempotency-Key`](/idempotency), a retry within 24 hours replays the original response, including the same `batchId`.

## Single submissions

For one-off submissions, [`POST /talent-network/candidates/submit`](/agency/api-reference/submit-talent-network-candidate) takes one candidate with the resume bytes inline (`resume.contentBase64`) instead of a `resumeFileId`. It returns `accepted`, `deduplicated`, `batchId`, and `submissionId`. Track it as a batch of one with the same batch endpoint.

## Before you submit

To avoid resubmitting people already in your network, check them first with [`POST /talent-network/candidates/lookup`](/agency/guides/network-membership#pre-flight-a-bulk-submit).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.