Skip to main content

Endpoint

POST /external-api/accounts/bulk-upsert

Base path

/external-api/accounts

What it does

Bulk upsert creates new accounts or updates existing accounts by externalId. The organization is resolved from the API key. Do not send organizationId in account objects.

Headers

  • Authorization: Bearer <org_api_key>
  • X-Cora-Timestamp: <unix_seconds_or_ms>
  • X-Cora-Signature: <hmac_sha256_hex>
  • Content-Type: application/json
See Authentication & Signing for the HMAC signing algorithm. The signed method for this endpoint is POST, and path_with_query is /external-api/accounts/bulk-upsert.

Curl example

curl -sS \
  -X POST "https://api.corafone.com/external-api/accounts/bulk-upsert" \
  -H "Authorization: Bearer cora_org_<keyId>.<secret>" \
  -H "X-Cora-Timestamp: <unix_seconds_or_ms>" \
  -H "X-Cora-Signature: <hmac_sha256_hex>" \
  -H "Content-Type: application/json" \
  --data '{"dryRun":false,"accounts":[{"externalId":"FILE_123","phoneNumber":"+15555550111","portfolioId":"PORTFOLIO_ID","bucket":"BUCKET_NAME","metadata":{"filenumber":"FILE_123"},"currentBalance":450.25}]}'

Request body

{
  "dryRun": false,
  "includeChanges": false,
  "accounts": [
    {
      "externalId": "FILE_123",
      "phoneNumber": "+15555550111",
      "portfolioId": "PORTFOLIO_ID",
      "bucket": "BUCKET_NAME",
      "metadata": {
        "filenumber": "FILE_123",
        "externalApiSyncState": "SYNCED"
      },
      "currentBalance": 450.25
    },
    {
      "externalId": "FILE_456",
      "currentBalance": 125.5
    }
  ]
}

Body fields

FieldTypeRequiredNotes
accountsAccountUpsert[]trueOne to 500 account objects.
dryRunbooleanfalseWhen true, validates and returns the same result shape without writing changes.
includeChangesbooleanfalseWhen true, changed existing accounts include a changes array in their result.

Account fields

Each account must include externalId. For new accounts, these fields are also required:
  • phoneNumber
  • portfolioId
  • metadata.filenumber
Bulk upsert accepts the same account update fields as Update one account, plus externalId, portfolioId, and optional bucket. portfolioId is used when creating a new account; it is not updated on existing accounts. bucket can be provided when creating a new account and can also be updated on existing accounts through bulk upsert.

Rules

  • Maximum 500 accounts per request.
  • Rate limited to 1 request every 20 seconds per API key/IP bucket.
  • Each externalId can appear only once per request.
  • Existing accounts are matched by externalId within the API key’s organization.
  • phoneNumber must be E.164 format, for example +15555550123.
  • Object fields such as metadata, person, and address are deep-merged when updating existing accounts.
  • Scalars and arrays replace existing values.
  • Results are partial-success: valid rows can be created or updated while other rows fail.

Dry run

Use dryRun: true to validate a batch and see which rows would be created, updated, unchanged, or failed.
{
  "dryRun": true,
  "includeChanges": true,
  "accounts": [
    {
      "externalId": "FILE_123",
      "phoneNumber": "+15555550111",
      "portfolioId": "PORTFOLIO_ID",
      "bucket": "BUCKET_NAME",
      "metadata": {
        "filenumber": "FILE_123"
      }
    }
  ]
}

Success response

The endpoint returns 200 when the bulk request is processed. Check the summary counts and per-row results to see which accounts succeeded or failed.
{
  "success": true,
  "code": "ACCOUNTS_BULK_UPSERT_COMPLETED",
  "message": "Bulk account upsert completed",
  "dryRun": false,
  "requested": 2,
  "created": 1,
  "updated": 1,
  "unchanged": 0,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "externalId": "FILE_123",
      "status": "created",
      "accountId": "ACCOUNT_ID",
      "updatedAt": "2026-02-23T12:45:00.000Z"
    },
    {
      "index": 1,
      "externalId": "FILE_456",
      "status": "updated",
      "accountId": "ACCOUNT_ID",
      "updatedAt": "2026-02-23T12:45:00.000Z"
    }
  ]
}

Result object

FieldTypeNotes
indexnumberZero-based index of the account in the request.
externalIdstringExternal account identifier, when available.
statuscreated | updated | unchanged | failedPer-row outcome.
accountIdstringCora account id for successful existing or created rows.
updatedAtISO datetime stringLast update timestamp when available.
codestringMachine-readable error code for failed rows.
messagestringHuman-readable error message for failed rows.
detailsobject[]Field-level details for validation failures.
changesobject[]Included for updated rows when includeChanges is true. Each change includes field, from, and to.

Change example

{
  "index": 1,
  "externalId": "FILE_456",
  "status": "updated",
  "accountId": "ACCOUNT_ID",
  "updatedAt": "2026-02-23T12:45:00.000Z",
  "changes": [
    {
      "field": "currentBalance",
      "from": 100,
      "to": 125.5
    }
  ]
}

Per-row failure example

{
  "index": 2,
  "externalId": "FILE_789",
  "status": "failed",
  "code": "MISSING_REQUIRED_FIELD",
  "message": "Missing required fields for account creation: phoneNumber, portfolioId, metadata.filenumber",
  "details": [
    {
      "field": "phoneNumber",
      "message": "phoneNumber is required when creating an account"
    },
    {
      "field": "portfolioId",
      "message": "portfolioId is required when creating an account"
    },
    {
      "field": "metadata.filenumber",
      "message": "metadata.filenumber is required when creating an account"
    }
  ]
}

Request-level errors

These errors stop the whole request:
  • INVALID_REQUEST_BODY: request body does not include an accounts array.
  • ACCOUNTS_REQUIRED: accounts is empty.
  • ACCOUNTS_LIMIT_EXCEEDED: more than 500 accounts were sent.
  • 429 Too Many Requests: more than 1 bulk-upsert request was sent within 20 seconds for the same API key/IP bucket.
  • MISSING_AUTH_HEADERS: one or more signed request auth headers are missing.
  • INVALID_API_KEY: API key is invalid, revoked, or not found.
  • REQUEST_TIMESTAMP_OUTSIDE_WINDOW: timestamp is outside the 5-minute window.
  • INVALID_REQUEST_SIGNATURE: HMAC signature does not match the request payload.
  • METHOD_NOT_ALLOWED: method is not POST.