# Create or Update Contacts in Bulk

POST https://api.smobi.com/v1/contacts/bulk

Authentication: x-api-key header (brand-scoped API key).

Upserts up to 100 contacts in one request. Items are processed independently: one failing item never aborts the batch — inspect per-item results. Duplicate phone numbers within a batch are rejected as a whole-request 400.

## Operation

```json
{
  "description": "Upserts up to 100 contacts in one request. Items are processed independently: one failing item never aborts the batch — inspect per-item results. Duplicate phone numbers within a batch are rejected as a whole-request 400.",
  "operationId": "bulkUpsertContacts",
  "parameters": [],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/BulkContactsDto"
        },
        "examples": {
          "initial-import": {
            "summary": "Initial import of your user base",
            "description": "Chunk larger datasets into batches of 100; each request returns per-item results so you can retry only the failures.",
            "value": {
              "contacts": [
                {
                  "phoneNumber": "+14155552222",
                  "firstName": "Jane",
                  "tags": [
                    "plan-pro"
                  ],
                  "marketingConsent": "OPTED_IN"
                },
                {
                  "phoneNumber": "+14155552223",
                  "firstName": "Sam",
                  "customFields": {
                    "externalId": "user_8232"
                  }
                }
              ]
            }
          }
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Per-item results with summary counts",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/BulkContactsResponseDto"
          },
          "example": {
            "results": [
              {
                "index": 0,
                "phoneNumber": "+14155552222",
                "status": "created",
                "id": "9b2f1c3e-8a4d-4f6b-9c2e-1d3f5a7b9c0e"
              },
              {
                "index": 1,
                "phoneNumber": "+14155552223",
                "status": "created",
                "id": "2c4d6e8f-1234-4567-89ab-123456789abc"
              }
            ],
            "summary": {
              "total": 2,
              "created": 2,
              "updated": 0,
              "failed": 0
            }
          }
        }
      }
    },
    "400": {
      "description": "Validation error (bad envelope, batch too large, or duplicate phone numbers)"
    },
    "401": {
      "description": "Missing or invalid API key",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "statusCode": {
                "type": "number",
                "example": 401
              },
              "message": {
                "type": "string",
                "example": "Invalid API key"
              },
              "error": {
                "type": "string",
                "example": "Unauthorized"
              }
            }
          }
        }
      }
    },
    "403": {
      "description": "The API key is not scoped to a brand",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "statusCode": {
                "type": "number",
                "example": 403
              },
              "message": {
                "type": "string",
                "example": "This endpoint requires a brand-scoped API key"
              },
              "error": {
                "type": "string",
                "example": "Forbidden"
              }
            }
          }
        }
      }
    }
  },
  "summary": "Create or Update Contacts in Bulk",
  "tags": [
    "Contacts"
  ],
  "security": [
    {
      "x-api-key": []
    }
  ]
}
```

## Referenced Schemas

```json
{
  "BulkContactsDto": {
    "type": "object",
    "properties": {
      "contacts": {
        "description": "1-100 contacts; duplicate phone numbers in one batch are rejected",
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/CreateContactDto"
        },
        "minItems": 1,
        "maxItems": 100
      }
    },
    "required": [
      "contacts"
    ],
    "additionalProperties": false
  },
  "CreateContactDto": {
    "type": "object",
    "properties": {
      "phoneNumber": {
        "type": "string",
        "example": "+14155552222",
        "description": "E.164 (+1234567890) or digits-only (1234567890) format"
      },
      "firstName": {
        "type": "string",
        "example": "Jane",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "lastName": {
        "type": "string",
        "example": "Doe",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "email": {
        "type": "string",
        "example": "jane@example.com",
        "nullable": true,
        "format": "email",
        "maxLength": 255
      },
      "state": {
        "type": "string",
        "example": "CA",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "country": {
        "type": "string",
        "example": "US",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "dob": {
        "type": "string",
        "example": "1990-04-12",
        "description": "Date of birth in YYYY-MM-DD format. Must be a valid date in the past; null clears the value.",
        "nullable": true,
        "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
      },
      "gender": {
        "type": "string",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "preferredLanguage": {
        "type": "string",
        "example": "en",
        "nullable": true,
        "minLength": 1,
        "maxLength": 255
      },
      "tags": {
        "example": [
          "vip"
        ],
        "description": "Full replace on updates; usable in segment rules",
        "type": "array",
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "maxItems": 100
      },
      "customFields": {
        "type": "object",
        "additionalProperties": {
          "anyOf": [
            {
              "type": "string",
              "maxLength": 1000,
              "nullable": true
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            },
            {
              "type": "array",
              "maxItems": 100,
              "items": {
                "type": "string",
                "maxLength": 1000
              }
            }
          ]
        },
        "example": {
          "loyaltyTier": "gold"
        },
        "description": "Map of up to 50 fields. Values may be strings (up to 1,000 characters), numbers, booleans, arrays of up to 100 strings, or null. Nested objects are not accepted. Keys must match ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$; `__proto__`, `constructor`, and `prototype` are reserved. A null value deletes that key on update.",
        "maxProperties": 50
      },
      "marketingConsent": {
        "type": "string",
        "enum": [
          "OPTED_IN",
          "OPTED_OUT"
        ],
        "description": "Setting OPTED_IN attests that you collected the contact's marketing consent. OPTED_IN is rejected (422) when the contact previously opted out (e.g. texted STOP); OPTED_OUT is always accepted. Contacts default to UNKNOWN, and campaigns only send to OPTED_IN contacts."
      }
    },
    "required": [
      "phoneNumber"
    ],
    "additionalProperties": false
  },
  "BulkContactsResponseDto": {
    "type": "object",
    "properties": {
      "results": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/BulkContactItemResultDto"
        }
      },
      "summary": {
        "$ref": "#/components/schemas/BulkContactsSummaryDto"
      }
    },
    "required": [
      "results",
      "summary"
    ]
  },
  "BulkContactItemResultDto": {
    "type": "object",
    "properties": {
      "index": {
        "type": "number",
        "example": 0
      },
      "phoneNumber": {
        "type": "string",
        "example": "+14155552222"
      },
      "status": {
        "type": "string",
        "enum": [
          "created",
          "updated",
          "error"
        ]
      },
      "id": {
        "type": "string",
        "example": "9b2f1c3e-8a4d-4f6b-9c2e-1d3f5a7b9c0e"
      },
      "error": {
        "type": "string",
        "example": "Cannot opt in a contact that has opted out; the contact must opt back in themselves (e.g. by texting START)"
      }
    },
    "required": [
      "index",
      "phoneNumber",
      "status"
    ]
  },
  "BulkContactsSummaryDto": {
    "type": "object",
    "properties": {
      "total": {
        "type": "number",
        "example": 3
      },
      "created": {
        "type": "number",
        "example": 2
      },
      "updated": {
        "type": "number",
        "example": 0
      },
      "failed": {
        "type": "number",
        "example": 1
      }
    },
    "required": [
      "total",
      "created",
      "updated",
      "failed"
    ]
  }
}
```
